@filigran/chatbot 3.7.4 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/utils/index.ts","../../src/hooks/protocols/parseRestEvent.ts","../../src/hooks/protocols/parseLegacyEvent.ts","../../src/hooks/protocols/parseAgUiEvent.ts","../../src/hooks/useChat.ts","../../src/hooks/useAgents.ts","../../src/hooks/useConversations.ts","../../src/hooks/useSidebarResize.ts","../../src/hooks/useAwayCompletionNotice.ts","../../src/components/icons/AlertTriangleIcon.tsx","../../src/components/icons/ArrowRightLeftIcon.tsx","../../src/components/icons/AttachFileIcon.tsx","../../src/components/icons/BotIcon.tsx","../../src/components/icons/BrainIcon.tsx","../../src/components/icons/CheckCircleIcon.tsx","../../src/components/icons/CheckIcon.tsx","../../src/components/icons/ChevronDownIcon.tsx","../../src/components/icons/CloseIcon.tsx","../../src/components/icons/CopyIcon.tsx","../../src/components/icons/DatabaseIcon.tsx","../../src/components/icons/DefaultLogoIcon.tsx","../../src/components/icons/DownloadIcon.tsx","../../src/components/icons/EditIcon.tsx","../../src/components/icons/ExternalLinkIcon.tsx","../../src/components/icons/FileIcon.tsx","../../src/components/icons/FloatingIcon.tsx","../../src/components/icons/FullscreenExitIcon.tsx","../../src/components/icons/FullscreenIcon.tsx","../../src/components/icons/GamepadIcon.tsx","../../src/components/icons/GlobeIcon.tsx","../../src/components/icons/HistoryIcon.tsx","../../src/components/icons/InfoIcon.tsx","../../src/components/icons/MailIcon.tsx","../../src/components/icons/SearchIcon.tsx","../../src/components/icons/SendIcon.tsx","../../src/components/icons/SidebarIcon.tsx","../../src/components/icons/SparklesIcon.tsx","../../src/components/icons/StopCircleIcon.tsx","../../src/components/icons/TerminalIcon.tsx","../../src/components/icons/TrashIcon.tsx","../../src/components/icons/UserPlusIcon.tsx","../../src/components/icons/WrenchIcon.tsx","../../src/components/icons/XCircleIcon.tsx","../../src/components/Dropdown.tsx","../../src/hooks/useClickOutside.ts","../../src/components/Spinner.tsx","../../src/components/Tooltip.tsx","../../src/components/ChatHeader.tsx","../../src/components/ChatInput.tsx","../../src/components/ChatWaitingGame.tsx","../../src/components/ChatThinking.tsx","../../src/components/MarkdownMessage.tsx","../../src/components/ReasoningDetailsDialog.tsx","../../src/components/ChatMessages.tsx","../../src/components/ChatWelcome.tsx","../../src/components/ChatPanel.tsx","../../src/components/ChatToggleButton.tsx"],"sourcesContent":["export function hexAlpha(hex: string, alpha: number): string {\n const a = Math.round(alpha * 255)\n .toString(16)\n .padStart(2, '0');\n return `${hex}${a}`;\n}\n\n/**\n * Markdown has no native support for nesting fenced code blocks of the same\n * length: per the CommonMark spec, the first inner ``` closes the outer block,\n * so everything after it renders *outside* the code block. LLMs constantly hit\n * this — when an agent shows a prompt or a full markdown document inside a\n * ```markdown … ``` fence, that document's own ``` fences shatter the snippet\n * into alternating code / prose fragments.\n *\n * The robust, spec-compliant fix is to make the *outer* fence longer than any\n * fence it contains: a 4-backtick fence is only closed by a run of ≥4\n * backticks, so all inner 3-backtick fences become literal content and the\n * whole document renders as one clean, copyable code block.\n *\n * We act only on the unambiguous, dominant case — a 3-backtick opener whose\n * info string is a markup language (markdown / md / mdx / markup) that actually\n * contains nested fences — so already-correct markdown is never rewritten\n * (a correctly authored nested block already uses a 4+-backtick opener, which\n * we skip).\n */\nexport function hardenNestedCodeFences(raw: string): string {\n if (!raw) return raw;\n const lines = raw.split('\\n');\n const fenceRe = /^(\\s*)(`{3,})(.*)$/;\n const markupLang = /^(markdown|md|mdx|markup)\\b/i;\n\n let openerIdx = -1;\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].match(fenceRe);\n if (m && m[2].length === 3 && markupLang.test(m[3].trim())) {\n openerIdx = i;\n break;\n }\n }\n if (openerIdx === -1) return raw;\n\n let maxRun = 3;\n let nestedCount = 0;\n let lastBareFence = -1;\n for (let i = openerIdx + 1; i < lines.length; i++) {\n const m = lines[i].match(fenceRe);\n if (!m) continue;\n nestedCount++;\n maxRun = Math.max(maxRun, m[2].length);\n if (m[3].trim() === '') lastBareFence = i;\n }\n if (nestedCount === 0) return raw;\n\n const fence = '`'.repeat(Math.max(maxRun + 1, 4));\n const om = lines[openerIdx].match(fenceRe)!;\n lines[openerIdx] = `${om[1]}${fence}${om[3]}`;\n if (lastBareFence > openerIdx) {\n const cm = lines[lastBareFence].match(fenceRe)!;\n lines[lastBareFence] = `${cm[1]}${fence}`;\n }\n return lines.join('\\n');\n}\n\n/**\n * GFM renders a pipe table only when the delimiter row (`|---|---|`) has the\n * SAME number of columns as the header row. LLMs frequently miscount (e.g. a\n * 4-column header followed by a 3-column delimiter), and a server-side guard can\n * corrupt the delimiter — in either case the whole table silently degrades to\n * raw `| … |` text. This repairs a mismatched delimiter row to the header's\n * column count (preserving any alignment colons) so the table renders.\n *\n * It only rewrites a delimiter that is ACTUALLY mismatched, so already-valid\n * tables are never touched. Fenced code blocks are skipped, and setext headings\n * (underlines with no `|`) are never mistaken for a table.\n */\nexport function normalizeMarkdownTables(raw: string): string {\n if (!raw || raw.indexOf('|') === -1) return raw;\n const lines = raw.split('\\n');\n\n // Splits on unescaped `|` only. A manual walk (rather than a negative\n // lookbehind, which is unsupported on older engines and would throw at parse\n // time in an untranspiled ESNext bundle) keeps `\\|` inside a cell intact.\n const splitCells = (row: string): string[] => {\n let s = row.trim();\n if (s.startsWith('|')) s = s.slice(1);\n if (s.endsWith('|')) s = s.slice(0, -1);\n const cells: string[] = [];\n let current = '';\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (ch === '\\\\' && i + 1 < s.length) {\n current += ch + s[i + 1];\n i++;\n } else if (ch === '|') {\n cells.push(current);\n current = '';\n } else {\n current += ch;\n }\n }\n cells.push(current);\n return cells;\n };\n const isDelimiterRow = (row: string): boolean => row.includes('|') && /^\\s*\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)*\\|?\\s*$/.test(row);\n // Emit the canonical 3-hyphen delimiter form. remark-gfm accepts a single\n // hyphen, but `---` (with optional alignment colons) is the portable form\n // every Markdown renderer agrees on, so prefer it.\n const alignOf = (cell: string): string => {\n const c = cell.trim();\n const left = c.startsWith(':');\n const right = c.endsWith(':');\n return left && right ? ':---:' : right ? '---:' : left ? ':---' : '---';\n };\n\n // Track both the fence character AND its run length: per CommonMark a closing\n // fence must use the same character and be at least as long as the opener, so\n // a shorter run (```) must not close a longer one (````), and a closing fence\n // carries no info string. Optional blockquote / list-item markers are allowed\n // before the run so fences nested in those containers (e.g. `- ```) are still\n // recognised and the table inside them is left untouched.\n const fenceRe = /^\\s*(?:(?:>\\s?)|(?:[-*+]\\s+)|(?:\\d{1,9}[.)]\\s+))*(`{3,}|~{3,})(.*)$/;\n let fenceChar: string | null = null;\n let fenceLen = 0;\n // A pipe-table header can share its line with a list-item marker (e.g.\n // `- | a | b |`). Strip a leading list marker before counting columns so the\n // count matches the cells GFM sees inside the list item, not the marker.\n const listMarkerRe = /^\\s*(?:[-*+]\\s+|\\d{1,9}[.)]\\s+)/;\n for (let i = 0; i < lines.length - 1; i++) {\n const fenceMatch = lines[i].match(fenceRe);\n if (fenceMatch) {\n const run = fenceMatch[1];\n if (fenceChar === null) {\n fenceChar = run[0];\n fenceLen = run.length;\n } else if (run[0] === fenceChar && run.length >= fenceLen && fenceMatch[2].trim() === '') {\n fenceChar = null;\n fenceLen = 0;\n }\n continue;\n }\n if (fenceChar !== null) continue;\n\n const header = lines[i];\n const delim = lines[i + 1];\n if (!header.includes('|') || isDelimiterRow(header) || !isDelimiterRow(delim)) continue;\n\n // A table can start on a list-item line, so both the cells and the\n // delimiter's alignment live AFTER the marker. Anchor the rewritten\n // delimiter to that content offset (padding the marker width with spaces) so\n // it stays a continuation line of the list item: GFM drops a table whose\n // delimiter dedents away from its header. Without a marker this is just the\n // header's leading whitespace, so top-level tables are emitted unchanged.\n const markerMatch = header.match(listMarkerRe);\n const offset = markerMatch ? markerMatch[0].length : header.length - header.trimStart().length;\n const indent = markerMatch ? ' '.repeat(offset) : header.slice(0, offset);\n\n const headerCols = splitCells(header.slice(offset)).length;\n const delimCells = splitCells(delim);\n if (headerCols < 2 || delimCells.length === headerCols) continue;\n\n const aligns: string[] = [];\n for (let c = 0; c < headerCols; c++) aligns.push(delimCells[c] ? alignOf(delimCells[c]) : '---');\n lines[i + 1] = `${indent}| ${aligns.join(' | ')} |`;\n }\n return lines.join('\\n');\n}\n\nexport const identity = (key: string) => key;\n\n/**\n * Nearest chatbot panel root (`.filigran-chatbot`) for portal-based overlays\n * (tooltips, dropdowns, dialogs), so they stay inside the panel's stacking\n * context instead of competing with the host app's z-indexes. Falls back to\n * `document.body` when rendered outside a panel.\n */\nexport function findChatbotRoot(el: HTMLElement | null): HTMLElement {\n let node = el;\n while (node) {\n if (node.classList.contains('filigran-chatbot')) return node;\n node = node.parentElement;\n }\n return document.body;\n}\n\n/**\n * Compact relative-time label for the conversation history menu\n * (\"just now\", \"5m ago\", \"3h ago\", \"2d ago\", then a short date).\n * Returns an empty string for missing/unparseable timestamps so the row\n * simply omits the label instead of showing \"Invalid Date\".\n */\nexport function timeAgo(iso: string | undefined, t: (key: string) => string): string {\n if (!iso) return '';\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return '';\n const diffMs = Date.now() - then;\n const minutes = Math.floor(diffMs / 60_000);\n if (minutes < 1) return t('just now');\n if (minutes < 60) return `${minutes}${t('m ago')}`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}${t('h ago')}`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}${t('d ago')}`;\n return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\n/** Matches a complete `[[FILE:<id>]]` deliverable marker agents embed in prose. */\nconst FILE_MARKER_RE = /\\[\\[FILE:[^\\]]+\\]\\]/g;\n\n/**\n * Matches an INCOMPLETE marker at the very end of the string. SSE streams can\n * split a `[[FILE:<id>]]` token before the closing `]]` arrives, so the tail\n * may be `[[FILE`, `[[FILE:`, `[[FILE:abc`, or `[[FILE:abc]` mid-stream. We\n * anchor on the literal `[[FILE` prefix (which is vanishingly unlikely to\n * appear legitimately at the end of prose) so it never clips real content.\n */\nconst PARTIAL_FILE_MARKER_RE = /\\[\\[FILE(?::[^\\]]*)?\\]?$/;\n\n/**\n * Strip the `[[FILE:<id>]]` markers an agent embeds in its reply to point at\n * generated files. The actual files render as separate download chips, so the\n * raw markers must be removed from the prose. Complete markers are removed\n * anywhere; an incomplete marker at the end is also removed so a partially\n * streamed token never flickers as raw `[[FILE:...` text.\n *\n * When no marker is present the content is returned **untouched** — we must\n * not trim or collapse blank lines on ordinary prose, which would clobber\n * intentional leading/trailing whitespace (e.g. indented Markdown / code).\n * Whitespace is only normalized when a marker was actually removed.\n *\n * Applied to assistant content only — user-typed text is never touched, so a\n * user who literally types `[[FILE:x]]` still sees their own text.\n */\nexport function stripFileMarkers(content: string): string {\n if (!content) return content;\n const stripped = content.replace(FILE_MARKER_RE, '').replace(PARTIAL_FILE_MARKER_RE, '');\n if (stripped === content) return content;\n return stripped\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n/** An ordered piece of assistant content: prose text or a file marker. */\nexport type FileMarkerPart = { type: 'text'; value: string } | { type: 'file'; fileId: string };\n\n/**\n * Split assistant content into ordered text/file parts around complete\n * `[[FILE:<id>]]` markers, so the renderer can place each download card at the\n * marker's source position (preserving interleaved order). A trailing\n * incomplete marker (an SSE token split mid-stream) is removed from the final\n * text part so it never shows as raw `[[FILE:...` text.\n */\nexport function splitFileMarkers(content: string): FileMarkerPart[] {\n if (!content) return [];\n const parts: FileMarkerPart[] = [];\n const re = /\\[\\[FILE:([^\\]]+)\\]\\]/g;\n let lastIndex = 0;\n let match: RegExpExecArray | null = re.exec(content);\n while (match !== null) {\n if (match.index > lastIndex) {\n parts.push({ type: 'text', value: content.slice(lastIndex, match.index) });\n }\n parts.push({ type: 'file', fileId: match[1] });\n lastIndex = re.lastIndex;\n match = re.exec(content);\n }\n const tail = content.slice(lastIndex).replace(PARTIAL_FILE_MARKER_RE, '');\n if (tail) parts.push({ type: 'text', value: tail });\n return parts;\n}\n","import type { ChatAttachment, ToolCallTraceEntry, TransferChainEntry } from '../../types';\nimport type { ParsedAction, ProtocolContext } from './types';\n\n/**\n * Normalize the raw `attachments` array from a backend `done` event into\n * typed {@link ChatAttachment} objects. Defensive: skips non-object entries\n * and entries without a `file_id`. Returns `undefined` when there is nothing\n * renderable so the `done` action stays lean for backends without #810.\n */\nexport function parseAttachments(raw: unknown): ChatAttachment[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: ChatAttachment[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const a = item as Record<string, unknown>;\n const fileId = a.file_id;\n if (typeof fileId !== 'string' || !fileId) continue;\n out.push({\n fileId,\n filename: typeof a.filename === 'string' ? a.filename : 'file',\n type: typeof a.type === 'string' ? a.type : undefined,\n size: typeof a.size === 'number' ? a.size : undefined,\n contentType: typeof a.content_type === 'string' ? a.content_type : undefined,\n fileTag: a.file_tag === 'working_file' ? 'working_file' : 'download_file',\n });\n }\n return out.length > 0 ? out : undefined;\n}\n\n/**\n * Normalize the raw `tool_call_trace` array (from a `done` event or restored\n * session metadata) into typed {@link ToolCallTraceEntry} objects. Defensive:\n * skips entries without a `name`. Returns `undefined` when empty so the\n * reasoning-details dialog falls back to the flat tool-name list.\n */\nexport function parseToolCallTrace(raw: unknown): ToolCallTraceEntry[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: ToolCallTraceEntry[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const e = item as Record<string, unknown>;\n if (typeof e.name !== 'string' || !e.name) continue;\n out.push({\n name: e.name,\n input: typeof e.input === 'string' ? e.input : undefined,\n output: typeof e.output === 'string' ? e.output : undefined,\n // Only a boolean is honored; a missing/malformed value defaults to\n // success so unknown states never render a false failure icon.\n success: typeof e.success === 'boolean' ? e.success : true,\n });\n }\n return out.length > 0 ? out : undefined;\n}\n\n/**\n * Normalize the raw `transfer_chain` array (from a `done` event or restored\n * session metadata) into typed {@link TransferChainEntry} objects.\n */\nexport function parseTransferChain(raw: unknown): TransferChainEntry[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: TransferChainEntry[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const e = item as Record<string, unknown>;\n if (typeof e.agent_name !== 'string' || !e.agent_name) continue;\n out.push({\n agentId: typeof e.agent_id === 'string' ? e.agent_id : '',\n agentName: e.agent_name,\n });\n }\n return out.length > 0 ? out : undefined;\n}\n\n/**\n * Parse an XTM One (REST) SSE event into a normalized action.\n */\nexport function parseRestEvent(evt: Record<string, unknown>, ctx: ProtocolContext): ParsedAction {\n const type = evt.type as string | undefined;\n\n if (type === 'error') {\n return { action: 'error', content: (evt.content as string) || '' };\n }\n\n if (type === 'status') {\n const st = evt.status as string;\n if (st === 'tool_done' || st === 'wind_down') {\n return { action: 'noop' };\n }\n if (st === 'streaming') {\n return { action: 'status', status: 'streaming' };\n }\n if (st === 'thinking_text') {\n return { action: 'status', status: 'thinking_text', thinkingContent: evt.content as string };\n }\n if (st === 'tool_start') {\n ctx.hasUsedTools = true;\n return { action: 'status', status: 'tool_start', tools: evt.tools as string[] | undefined };\n }\n if (st === 'tool_heartbeat') {\n // Liveness signal during a long tool execution (background tasks,\n // consults, big integration calls): carries the elapsed seconds but\n // no new semantic state — the consumer must keep its current label.\n return {\n action: 'status',\n status: 'tool_heartbeat',\n tools: evt.tools as string[] | undefined,\n elapsedS: typeof evt.elapsed_s === 'number' ? evt.elapsed_s : undefined,\n };\n }\n if (st === 'thinking' && ctx.hasUsedTools) {\n return { action: 'status', status: 'analyzing' };\n }\n return { action: 'status', status: st, tools: evt.tools as string[] | undefined };\n }\n\n if (type === 'stream') {\n return { action: 'stream', content: evt.content as string };\n }\n\n if (type === 'done') {\n return {\n action: 'done',\n content: evt.content as string,\n conversationId: evt.conversation_id as string | undefined,\n toolNames: evt.tool_names as string[] | undefined,\n toolCallCount: evt.tool_call_count as number | undefined,\n iterations: evt.iterations as number | undefined,\n transferAgentId: evt.transfer_agent_id as string | undefined,\n transferAgentName: evt.transfer_agent_name as string | undefined,\n attachments: parseAttachments(evt.attachments),\n reasoning: typeof evt.reasoning === 'string' ? evt.reasoning : undefined,\n toolCallTrace: parseToolCallTrace(evt.tool_call_trace),\n transferChain: parseTransferChain(evt.transfer_chain),\n isTruncated: evt.is_truncated === true || undefined,\n };\n }\n\n return { action: 'noop' };\n}\n","import type { ParsedAction, ProtocolContext } from './types';\n\n/**\n * Parse a Flowise-style SSE event into a normalized action.\n */\nexport function parseLegacyEvent(evt: Record<string, unknown>, ctx: ProtocolContext): ParsedAction {\n const eventType = evt.event as string | undefined;\n\n if (eventType === 'nextAgentFlow') {\n const data = evt.data as Record<string, unknown> | undefined;\n const nodeId = data?.nodeId as string | undefined;\n if (data?.status === 'INPROGRESS' && nodeId) {\n ctx.activeNodeId = nodeId;\n }\n return { action: 'noop' };\n }\n\n if (eventType === 'start') {\n return { action: 'noop' };\n }\n\n if (eventType === 'token') {\n const tokenData = ((evt.data as string) ?? '').replace(/<br\\s*\\/?>/g, '\\n');\n return { action: 'stream', content: tokenData };\n }\n\n if (eventType === 'agentReasoning') {\n const reasoning = evt.data as Record<string, unknown> | undefined;\n const usedTools = reasoning?.usedTools as Array<{ tool: string }> | undefined;\n if (usedTools?.length) {\n ctx.hasUsedTools = true;\n return { action: 'status', status: 'tool_start', tools: usedTools.map((t) => t.tool) };\n }\n if (ctx.hasUsedTools) {\n return { action: 'status', status: 'analyzing' };\n }\n return { action: 'status', status: 'thinking' };\n }\n\n if (eventType === 'usedTools') {\n ctx.hasUsedTools = true;\n const data = evt.data as Array<{ tool: string }> | undefined;\n const toolNames = Array.isArray(data) ? data.map((t) => t.tool) : [];\n return { action: 'status', status: 'tool_start', tools: toolNames };\n }\n\n if (eventType === 'metadata') {\n const data = evt.data as Record<string, unknown> | undefined;\n const chatId = data?.chatId as string | undefined;\n if (chatId) {\n return { action: 'set_chat_id', chatId };\n }\n return { action: 'noop' };\n }\n\n if (eventType === 'error') {\n return { action: 'error', content: (evt.data as string) || '' };\n }\n\n if (eventType === 'end') {\n return { action: 'done', content: '' };\n }\n\n return { action: 'noop' };\n}\n","import type { ParsedAction, ProtocolContext } from './types';\n\n/**\n * AG-UI protocol event types.\n * @see https://github.com/ag-ui-protocol/ag-ui\n */\n\n/**\n * Parse an AG-UI protocol SSE event into a normalized action.\n *\n * AG-UI uses a Start/Content/End lifecycle for messages and tool calls.\n * We map these to the same internal actions used by the other protocols.\n */\nexport function parseAgUiEvent(evt: Record<string, unknown>, ctx: ProtocolContext): ParsedAction {\n const type = evt.type as string | undefined;\n\n // --- Run lifecycle ---\n\n if (type === 'RUN_STARTED') {\n return { action: 'status', status: 'thinking' };\n }\n\n if (type === 'RUN_FINISHED') {\n return { action: 'done', content: '' };\n }\n\n if (type === 'RUN_ERROR') {\n return { action: 'error', content: (evt.message as string) || 'Unknown error' };\n }\n\n // --- Step lifecycle ---\n\n if (type === 'STEP_STARTED') {\n const stepName = evt.stepName as string | undefined;\n return { action: 'status', status: stepName || 'thinking' };\n }\n\n if (type === 'STEP_FINISHED') {\n return { action: 'noop' };\n }\n\n // --- Text message streaming ---\n\n if (type === 'TEXT_MESSAGE_START') {\n return { action: 'status', status: 'streaming' };\n }\n\n if (type === 'TEXT_MESSAGE_CONTENT') {\n const delta = evt.delta as string;\n if (delta) {\n return { action: 'stream', content: delta };\n }\n return { action: 'noop' };\n }\n\n if (type === 'TEXT_MESSAGE_END') {\n return { action: 'noop' };\n }\n\n // TEXT_MESSAGE_CHUNK is a convenience event that combines Start+Content+End\n if (type === 'TEXT_MESSAGE_CHUNK') {\n const delta = evt.delta as string | undefined;\n if (delta) {\n return { action: 'stream', content: delta };\n }\n return { action: 'noop' };\n }\n\n // --- Tool call lifecycle ---\n\n if (type === 'TOOL_CALL_START') {\n ctx.hasUsedTools = true;\n const toolName = evt.toolCallName as string | undefined;\n return { action: 'status', status: 'tool_start', tools: toolName ? [toolName] : [] };\n }\n\n if (type === 'TOOL_CALL_ARGS') {\n // Tool arguments streaming — no UI equivalent, skip\n return { action: 'noop' };\n }\n\n if (type === 'TOOL_CALL_END') {\n return { action: 'status', status: 'analyzing' };\n }\n\n if (type === 'TOOL_CALL_RESULT') {\n // Tool result — no direct UI mapping, skip\n return { action: 'noop' };\n }\n\n if (type === 'TOOL_CALL_CHUNK') {\n // Convenience form — treat like TOOL_CALL_START if it has a name\n const toolName = evt.toolCallName as string | undefined;\n if (toolName) {\n ctx.hasUsedTools = true;\n return { action: 'status', status: 'tool_start', tools: [toolName] };\n }\n return { action: 'noop' };\n }\n\n // --- Reasoning / thinking ---\n\n if (type === 'REASONING_START' || type === 'REASONING_MESSAGE_START') {\n return { action: 'status', status: 'thinking' };\n }\n\n if (type === 'REASONING_MESSAGE_CONTENT' || type === 'REASONING_MESSAGE_CHUNK') {\n // Reasoning text — surface it in the dedicated thinking pane\n const delta = evt.delta as string | undefined;\n if (delta) {\n return { action: 'status', status: 'thinking_text', thinkingContent: delta };\n }\n return { action: 'status', status: 'thinking' };\n }\n\n if (type === 'REASONING_MESSAGE_END' || type === 'REASONING_END' || type === 'REASONING_ENCRYPTED_VALUE') {\n return { action: 'noop' };\n }\n\n // --- State management ---\n\n if (type === 'STATE_SNAPSHOT' || type === 'STATE_DELTA' || type === 'MESSAGES_SNAPSHOT') {\n // State sync — not mapped to chat UI currently\n return { action: 'noop' };\n }\n\n // --- Activity events ---\n\n if (type === 'ACTIVITY_SNAPSHOT' || type === 'ACTIVITY_DELTA') {\n return { action: 'noop' };\n }\n\n // --- Pass-through / custom ---\n\n if (type === 'RAW' || type === 'CUSTOM') {\n return { action: 'noop' };\n }\n\n return { action: 'noop' };\n}\n","import { useCallback, useRef, useState } from 'react';\nimport type { AgentStatusState, ApiEndpoints, BackendType, ChatFile, ChatMessage } from '../types';\nimport type { ParsedAction, ProtocolContext } from './protocols';\nimport { parseAgUiEvent, parseLegacyEvent, parseRestEvent } from './protocols';\n\nconst STORAGE_KEY = 'filigranChatConversationId';\nconst LEGACY_CHAT_ID_KEY = 'filigranChatLegacyChatId';\n\n/** Maximum number of files that can be attached to a single message. */\nconst DEFAULT_MAX_FILE_COUNT = 10;\n/** Maximum total size of all attached files (50 MB). */\nconst DEFAULT_MAX_TOTAL_SIZE = 50 * 1024 * 1024;\n\ninterface UseChatOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n agentSlug: string | null | undefined;\n requestHeaders?: Record<string, string>;\n /** Arbitrary host page/application context, sent as `context` on the REST message body. */\n pageContext?: Record<string, unknown>;\n t: (key: string) => string;\n maxFileCount?: number;\n maxTotalSize?: number;\n}\n\nexport interface TransferredAgent {\n id: string;\n name: string;\n}\n\ninterface UseChatReturn {\n messages: ChatMessage[];\n inputValue: string;\n setInputValue: (value: string) => void;\n isLoading: boolean;\n agentStatus: AgentStatusState | null;\n attachedFiles: ChatFile[];\n conversationId: string | null;\n transferredAgent: TransferredAgent | null;\n /**\n * True while a response is streaming AND the typed text can be dispatched\n * immediately as a mid-run steering message (REST backend with a steer\n * endpoint and a known conversation id). Gates the steering affordances in\n * the composer (accent Send next to Stop, \"Enter to send now\" copy).\n */\n canSteer: boolean;\n historyLoadedRef: React.MutableRefObject<boolean>;\n /**\n * Ref mirror of {@link conversationId}, always current across async\n * boundaries. Exposed so the history-restore effect can tell, when its\n * `/chat/sessions` response arrives, whether the conversation it was issued\n * for is still the active one — and ignore a genuinely superseded response\n * (new chat / agent switch) without discarding a restore that was merely\n * torn down by a benign host re-render or a StrictMode double-invoke.\n */\n conversationIdRef: React.MutableRefObject<string | null>;\n handleFileAdd: (fileList: FileList | null) => void;\n handlePaste: (e: React.ClipboardEvent) => void;\n handleSendMessage: () => Promise<void>;\n handleNewChat: () => void;\n handleStopGenerating: () => void;\n setAttachedFiles: React.Dispatch<React.SetStateAction<ChatFile[]>>;\n setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>;\n /**\n * Set (or clear) the active conversation id, keeping React state, the\n * cross-async-boundary ref mirror, and localStorage all in sync. Pass\n * `null` to reset. Prefer this over a raw state setter so the id consumed\n * by `handleSendMessage` (which reads the ref) never drifts from what the\n * UI shows.\n */\n updateConversationId: (id: string | null) => void;\n /**\n * Switch to another existing conversation (history menu). Aborts any\n * in-flight request, clears the transcript, and re-arms the history-restore\n * effect so the selected conversation's messages are fetched via the\n * sessions endpoint.\n */\n handleSwitchConversation: (id: string) => void;\n}\n\nfunction getParser(backendType: BackendType): (evt: Record<string, unknown>, ctx: ProtocolContext) => ParsedAction {\n switch (backendType) {\n case 'legacy':\n return parseLegacyEvent;\n case 'ag-ui':\n return parseAgUiEvent;\n default:\n return parseRestEvent;\n }\n}\n\nfunction buildRequestBody(\n backendType: BackendType,\n content: string,\n opts: {\n legacyChatId: string | null;\n conversationId: string | null;\n agentSlug: string | null | undefined;\n pageContext?: Record<string, unknown>;\n },\n): Record<string, unknown> {\n switch (backendType) {\n case 'legacy':\n return { question: content, chatId: opts.legacyChatId ?? undefined, streaming: true };\n case 'ag-ui':\n return {\n threadId: opts.conversationId ?? crypto.randomUUID(),\n runId: crypto.randomUUID(),\n messages: [{ id: crypto.randomUUID(), role: 'user', content }],\n tools: [],\n context: [],\n state: {},\n forwardedProps: opts.agentSlug ? { agentSlug: opts.agentSlug } : {},\n };\n default: {\n const body: Record<string, unknown> = { content, conversation_id: opts.conversationId, agent_slug: opts.agentSlug };\n // Forward arbitrary host page context (e.g. current URL) so the agent\n // knows where the user is. Omitted when empty to keep payloads lean.\n // Guard serialization: the whole body is later JSON.stringify'd, so a\n // non-serializable value (circular reference, BigInt, …) would otherwise\n // throw and break the message send. Drop the context instead — page\n // context is supplementary and must never prevent a message from going out.\n // Decide using the serialized result so values that normalize to an empty\n // object (e.g. `{ url: undefined }`, `{ fn: () => {} }`) are also omitted.\n if (opts.pageContext && Object.keys(opts.pageContext).length > 0) {\n try {\n const serialized = JSON.stringify(opts.pageContext);\n if (serialized && serialized !== '{}') {\n body.context = opts.pageContext;\n }\n } catch {\n // Non-serializable page context — skip it rather than fail the send.\n }\n }\n return body;\n }\n }\n}\n\nexport function useChat({\n apiBaseUrl,\n apiEndpoints,\n backendType = 'rest',\n agentSlug,\n requestHeaders,\n pageContext,\n t,\n maxFileCount = DEFAULT_MAX_FILE_COUNT,\n maxTotalSize = DEFAULT_MAX_TOTAL_SIZE,\n}: UseChatOptions): UseChatReturn {\n const isLegacy = backendType === 'legacy';\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [inputValue, setInputValue] = useState('');\n const [isLoading, setIsLoading] = useState(false);\n const [agentStatus, setAgentStatus] = useState<AgentStatusState | null>(null);\n const [conversationId, setConversationId] = useState<string | null>(() => {\n if (typeof window === 'undefined') return null;\n return localStorage.getItem(STORAGE_KEY);\n });\n const [attachedFiles, setAttachedFiles] = useState<ChatFile[]>([]);\n const [transferredAgent, setTransferredAgent] = useState<TransferredAgent | null>(null);\n const [legacyChatId, setLegacyChatId] = useState<string | null>(() => {\n if (typeof window === 'undefined') return null;\n return localStorage.getItem(LEGACY_CHAT_ID_KEY);\n });\n\n const historyLoadedRef = useRef(false);\n const abortControllerRef = useRef<AbortController | null>(null);\n const hasUsedToolsRef = useRef(false);\n // Ref mirror of conversationId — always current across async boundaries\n const conversationIdRef = useRef(conversationId);\n // Ref mirror of pageContext so the value sent reflects the page the user is\n // on at send time, regardless of when the send handler closure was created.\n const pageContextRef = useRef(pageContext);\n pageContextRef.current = pageContext;\n // Mutex to prevent concurrent session creation\n const creatingSessionRef = useRef<Promise<string | null> | null>(null);\n // Abort controller for in-flight file uploads (cancelled on new chat)\n const uploadAbortRef = useRef<AbortController>(new AbortController());\n\n // Guard invalid consumer values and keep deterministic limits.\n const effectiveMaxFileCount = Number.isFinite(maxFileCount) && maxFileCount > 0 ? Math.floor(maxFileCount) : DEFAULT_MAX_FILE_COUNT;\n const effectiveMaxTotalSize = Number.isFinite(maxTotalSize) && maxTotalSize > 0 ? maxTotalSize : DEFAULT_MAX_TOTAL_SIZE;\n\n // Determine message endpoint URL\n const getMessagesUrl = () => {\n if (isLegacy || apiEndpoints?.singleEndpoint) {\n return apiBaseUrl; // POST directly to base URL\n }\n return `${apiBaseUrl}${apiEndpoints?.messages ?? '/chat/messages'}`;\n };\n\n // Determine mid-run steering endpoint URL (null disables steering)\n const getSteerUrl = (): string | null => {\n if (isLegacy || backendType === 'ag-ui' || apiEndpoints?.singleEndpoint || apiEndpoints?.steer === null) {\n return null;\n }\n return `${apiBaseUrl}${apiEndpoints?.steer ?? '/chat/messages/steer'}`;\n };\n\n // Determine upload endpoint URL (null disables file upload proxying)\n const getUploadUrl = (): string | null => {\n if (isLegacy || apiEndpoints?.singleEndpoint || apiEndpoints?.upload === null) {\n return null;\n }\n return `${apiBaseUrl}${apiEndpoints?.upload ?? '/chat/upload'}`;\n };\n\n // Determine sessions endpoint URL\n const getSessionsUrl = (): string | null => {\n if (isLegacy || apiEndpoints?.singleEndpoint || apiEndpoints?.sessions === null) {\n return null;\n }\n return `${apiBaseUrl}${apiEndpoints?.sessions ?? '/chat/sessions'}`;\n };\n\n /**\n * Update conversationId in React state, the ref mirror, and localStorage.\n * Stable identity (useCallback) so it can be used as an effect dependency.\n */\n const updateConversationId = useCallback((id: string | null) => {\n conversationIdRef.current = id;\n setConversationId(id);\n if (id) {\n localStorage.setItem(STORAGE_KEY, id);\n } else {\n localStorage.removeItem(STORAGE_KEY);\n }\n }, []);\n\n /**\n * Ensure a conversation exists. Uses a mutex so concurrent callers\n * (e.g. multiple files selected at once) share a single session creation.\n */\n const ensureConversation = async (slug: string | null | undefined): Promise<string | null> => {\n // Fast path: already have one\n if (conversationIdRef.current) return conversationIdRef.current;\n\n // If another call is already creating, wait for it\n if (creatingSessionRef.current) return creatingSessionRef.current;\n\n const sessionsUrl = getSessionsUrl();\n if (!sessionsUrl) return null;\n\n const promise = (async () => {\n try {\n const res = await fetch(sessionsUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({ agent_slug: slug }),\n });\n if (!res.ok) return null;\n const data = await res.json();\n const convId = (data?.conversation_id as string) ?? null;\n if (convId) {\n updateConversationId(convId);\n }\n return convId;\n } catch {\n return null;\n } finally {\n creatingSessionRef.current = null;\n }\n })();\n\n creatingSessionRef.current = promise;\n return promise;\n };\n\n /**\n * Upload a single file to the backend and return its file_id.\n */\n const uploadSingleFile = async (file: File, convId: string, signal: AbortSignal): Promise<string> => {\n const uploadUrl = getUploadUrl()!;\n const formData = new FormData();\n formData.append('conversation_id', convId);\n formData.append('file', file, file.name);\n\n const uploadHeaders = requestHeaders\n ? Object.fromEntries(\n Object.entries(requestHeaders).filter(([k]) => {\n const key = k.toLowerCase();\n return key !== 'content-type';\n }),\n )\n : undefined;\n\n const res = await fetch(uploadUrl, {\n method: 'POST',\n headers: uploadHeaders,\n body: formData,\n signal,\n });\n if (!res.ok) {\n throw new Error(`File upload failed: ${res.status}`);\n }\n const data = await res.json();\n const ids: string[] = data.file_ids ?? [];\n if (ids.length === 0) throw new Error('No file_id returned');\n return ids[0];\n };\n\n /**\n * Handle file selection: validate limits, add files to state immediately,\n * then upload them in the background. Each file chip shows its upload status.\n */\n const handleFileAdd = (fileList: FileList | null) => {\n if (!fileList || fileList.length === 0 || !getUploadUrl()) return;\n\n // Build the list of accepted files outside the state updater (pure logic)\n const incoming = Array.from(fileList);\n\n // We need current state to check limits — use a ref-like approach:\n // read attachedFiles via a one-shot updater that returns prev unchanged,\n // then compute outside. Simpler: just compute optimistically and let the\n // updater do the final gating.\n\n // Pre-generate stable IDs and entries so side effects use the same IDs\n const candidates: { file: File; tempId: string }[] = incoming.map((file) => ({\n file,\n tempId: crypto.randomUUID(),\n }));\n\n // Update state (pure — no side effects)\n let accepted: { file: File; tempId: string }[] = [];\n setAttachedFiles((prev) => {\n const currentCount = prev.length;\n const currentSize = prev.reduce((sum, f) => sum + f.size, 0);\n\n const slotsAvailable = effectiveMaxFileCount - currentCount;\n if (slotsAvailable <= 0) return prev;\n\n let sizeLeft = effectiveMaxTotalSize - currentSize;\n const filtered: { file: File; tempId: string }[] = [];\n for (const c of candidates.slice(0, slotsAvailable)) {\n if (c.file.size <= sizeLeft) {\n filtered.push(c);\n sizeLeft -= c.file.size;\n }\n }\n if (filtered.length === 0) return prev;\n\n accepted = filtered;\n\n const newEntries: ChatFile[] = filtered.map(({ file, tempId }) => ({\n name: file.name,\n type: file.type,\n size: file.size,\n rawFile: file,\n uploadStatus: 'pending' as const,\n fileId: tempId,\n }));\n\n return [...prev, ...newEntries];\n });\n\n // Launch uploads OUTSIDE the state updater (side effects)\n // Use setTimeout(0) to ensure state has settled after the updater\n setTimeout(() => {\n const signal = uploadAbortRef.current.signal;\n for (const { file, tempId } of accepted) {\n (async () => {\n try {\n const convId = await ensureConversation(agentSlug);\n if (!convId) {\n setAttachedFiles((p) => p.map((f) => (f.fileId === tempId ? { ...f, uploadStatus: 'error' } : f)));\n return;\n }\n const fileId = await uploadSingleFile(file, convId, signal);\n setAttachedFiles((p) => p.map((f) => (f.fileId === tempId ? { ...f, fileId, uploadStatus: 'done' } : f)));\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') return;\n setAttachedFiles((p) => p.map((f) => (f.fileId === tempId ? { ...f, uploadStatus: 'error' } : f)));\n }\n })();\n }\n }, 0);\n };\n\n const handlePaste = (e: React.ClipboardEvent) => {\n const { files } = e.clipboardData;\n if (files.length > 0) {\n e.preventDefault();\n handleFileAdd(files);\n }\n };\n\n /**\n * Mid-run steering: dispatch a message while the agent is still generating.\n * The user bubble is added optimistically and the steer endpoint is POSTed;\n * the backend persists the message and injects it into the running agentic\n * loop at the next iteration boundary. On failure (network error, or a\n * backend without steering support answering non-2xx) the optimistic bubble\n * is rolled back and the text is restored into the composer — prepended on\n * its own line if the user already typed something new — so the message is\n * never silently lost and never resets the in-flight run state.\n */\n const steerMessage = async (content: string) => {\n const steerUrl = getSteerUrl();\n const convId = conversationIdRef.current;\n if (!steerUrl || !convId) return;\n\n const optimistic: ChatMessage = {\n id: crypto.randomUUID(),\n role: 'user',\n content,\n timestamp: new Date(),\n };\n setMessages((prev) => [...prev, optimistic]);\n\n try {\n const res = await fetch(steerUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({ conversation_id: convId, content, agent_slug: agentSlug }),\n });\n if (!res.ok) throw new Error(`Steer failed: ${res.status}`);\n } catch {\n setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));\n setInputValue((prev) => (prev ? `${content}\\n${prev}` : content));\n }\n };\n\n const handleSendMessage = async () => {\n const steerText = inputValue.trim();\n if (isLoading) {\n // Mid-run steering — text-only sends while a response is streaming.\n // Attachments keep the legacy wait behavior (the upload + message pair\n // cannot be injected into a running loop).\n if (steerText && attachedFiles.length === 0 && getSteerUrl() && conversationIdRef.current) {\n setInputValue('');\n await steerMessage(steerText);\n }\n return;\n }\n if (!inputValue.trim() && attachedFiles.length === 0) return;\n const content = inputValue.trim();\n\n const userMsg: ChatMessage = {\n id: crypto.randomUUID(),\n role: 'user',\n content,\n timestamp: new Date(),\n files: attachedFiles.length > 0 ? [...attachedFiles] : undefined,\n };\n setMessages((prev) => [...prev, userMsg]);\n setInputValue('');\n // Clear attachment chips after sending so the input returns to a clean state.\n setAttachedFiles([]);\n setIsLoading(true);\n setAgentStatus({ status: 'thinking' });\n hasUsedToolsRef.current = false;\n\n const assistantId = crypto.randomUUID();\n setMessages((prev) => [...prev, { id: assistantId, role: 'assistant', content: '', timestamp: new Date() }]);\n\n // The assistant message currently being streamed into. A steered turn can\n // produce multiple response segments on one SSE stream: the backend\n // completes the current segment (intermediate `done`), then runs a\n // follow-up pass for the steering message (fresh `thinking` + `stream`\n // events). Each segment gets its own assistant bubble. Declared outside\n // the try so the catch below writes the error into the LIVE segment, not\n // an already-completed one.\n let currentAssistantId = assistantId;\n\n try {\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n // Collect file_ids from already-uploaded files (uploaded eagerly on selection)\n const fileIds = (userMsg.files ?? []).filter((f) => f.uploadStatus === 'done' && f.fileId).map((f) => f.fileId!);\n\n // Step 1: Send the message (with file_ids if files were uploaded)\n // Use conversationIdRef to get the latest value (may have been set by eager upload)\n const requestBody = buildRequestBody(backendType, content, {\n legacyChatId,\n conversationId: conversationIdRef.current,\n agentSlug,\n pageContext: pageContextRef.current,\n });\n if (fileIds.length > 0) {\n (requestBody as Record<string, unknown>).file_ids = fileIds;\n }\n\n setAgentStatus({ status: 'thinking' });\n\n const res = await fetch(getMessagesUrl(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n });\n\n if (!res.ok || !res.body) {\n setMessages((prev) =>\n prev.map((m) => (m.id === assistantId ? { ...m, content: t('Unable to connect. Please check the configuration.') } : m)),\n );\n return;\n }\n\n const parseEvent = getParser(backendType);\n const ctx: ProtocolContext = { hasUsedTools: false, activeNodeId: '' };\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let accumulated = '';\n let doneReceived = false;\n\n /**\n * Open a new response segment when events keep flowing after a `done`.\n * Appends a fresh empty assistant message (after any steered user\n * bubble) and resets the per-segment accumulators. The fresh `thinking`\n * status also clears the reasoning window — each segment carries its\n * own reasoning, mirroring the web chat behavior.\n */\n const ensureSegment = () => {\n if (!doneReceived) return;\n doneReceived = false;\n accumulated = '';\n currentAssistantId = crypto.randomUUID();\n const segmentId = currentAssistantId;\n setMessages((prev) => [...prev, { id: segmentId, role: 'assistant', content: '', timestamp: new Date() }]);\n setAgentStatus({ status: 'thinking' });\n };\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n for (const rawLine of lines) {\n const line = rawLine.replace(/\\r$/, '');\n if (!line.startsWith('data:')) continue;\n const jsonStr = line.startsWith('data: ') ? line.slice(6) : line.slice(5);\n try {\n const evt = JSON.parse(jsonStr) as Record<string, unknown>;\n const parsed: ParsedAction = parseEvent(evt, ctx);\n\n // Sync ref → context for cross-event tracking\n ctx.hasUsedTools = ctx.hasUsedTools || hasUsedToolsRef.current;\n\n switch (parsed.action) {\n case 'status': {\n ensureSegment();\n const segId = currentAssistantId;\n if (parsed.status === 'tool_start') hasUsedToolsRef.current = true;\n if (parsed.status === 'stream_retract') {\n // Rare: text that streamed as a provisional answer turned\n // out to precede tool calls — discard the answer bubble\n // (the text re-arrives as thinking_text right after, so it\n // lands in the reasoning window instead).\n accumulated = '';\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: '' } : m)));\n setAgentStatus((prev) => ({\n status: 'analyzing',\n thinkingContent: prev?.thinkingContent,\n }));\n } else if (parsed.status === 'thinking_text') {\n setAgentStatus((prev) => ({\n ...prev,\n status: prev?.status ?? 'thinking',\n thinkingContent: (prev?.thinkingContent ?? '') + (parsed.thinkingContent ?? ''),\n }));\n } else if (parsed.status === 'tool_heartbeat') {\n // Liveness signal during a long tool execution: update the\n // elapsed counter but KEEP the current status label/tools —\n // replacing the status would flip e.g. \"Waiting for\n // background task…\" back to \"Thinking…\" mid-execution.\n setAgentStatus((prev) =>\n prev ? { ...prev, elapsedS: parsed.elapsedS } : { status: 'tool_start', tools: parsed.tools, elapsedS: parsed.elapsedS },\n );\n } else {\n setAgentStatus((prev) => ({\n status: parsed.status,\n tools: parsed.tools,\n thinkingContent: prev?.thinkingContent,\n }));\n }\n break;\n }\n\n case 'stream': {\n ensureSegment();\n accumulated += parsed.content;\n // Snapshot the segment id and text: the state updater runs\n // asynchronously and `currentAssistantId` / `accumulated` may\n // already belong to the NEXT segment by then.\n const segId = currentAssistantId;\n const text = accumulated;\n setAgentStatus((prev) => ({ status: 'streaming', thinkingContent: prev?.thinkingContent }));\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: text } : m)));\n break;\n }\n\n case 'done': {\n doneReceived = true;\n if (parsed.conversationId) {\n updateConversationId(parsed.conversationId);\n }\n if (parsed.transferAgentId && parsed.transferAgentName) {\n setTransferredAgent({ id: parsed.transferAgentId, name: parsed.transferAgentName });\n }\n const segId = currentAssistantId;\n const finalContent = parsed.content || accumulated;\n setMessages((prev) =>\n prev.map((m) =>\n m.id === segId\n ? {\n ...m,\n content: finalContent,\n toolNames: parsed.toolNames,\n toolCallCount: parsed.toolCallCount,\n iterations: parsed.iterations,\n attachments: parsed.attachments,\n reasoning: parsed.reasoning,\n toolCallTrace: parsed.toolCallTrace,\n transferChain: parsed.transferChain,\n isTruncated: parsed.isTruncated,\n }\n : m,\n ),\n );\n break;\n }\n\n case 'error': {\n ensureSegment();\n const segId = currentAssistantId;\n setMessages((prev) =>\n prev.map((m) =>\n m.id === segId ? { ...m, content: parsed.content || t('Unable to connect. Please check the configuration.') } : m,\n ),\n );\n return;\n }\n\n case 'set_chat_id':\n setLegacyChatId(parsed.chatId);\n localStorage.setItem(LEGACY_CHAT_ID_KEY, parsed.chatId);\n break;\n\n case 'noop':\n break;\n }\n\n // Keep ref in sync with context\n hasUsedToolsRef.current = ctx.hasUsedTools;\n } catch {\n /* skip malformed SSE */\n }\n }\n }\n if (accumulated && !doneReceived) {\n const segId = currentAssistantId;\n const text = accumulated;\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: text || 'No response.' } : m)));\n }\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') return;\n const segId = currentAssistantId;\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: t('Sorry, an error occurred. Please try again.') } : m)));\n } finally {\n abortControllerRef.current = null;\n setIsLoading(false);\n setAgentStatus(null);\n hasUsedToolsRef.current = false;\n }\n };\n\n const handleNewChat = () => {\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n // Cancel any in-flight file uploads\n uploadAbortRef.current.abort();\n uploadAbortRef.current = new AbortController();\n creatingSessionRef.current = null;\n setMessages([]);\n setInputValue('');\n setAttachedFiles([]);\n setIsLoading(false);\n setAgentStatus(null);\n setTransferredAgent(null);\n hasUsedToolsRef.current = false;\n historyLoadedRef.current = false;\n if (isLegacy) {\n setLegacyChatId(null);\n localStorage.removeItem(LEGACY_CHAT_ID_KEY);\n } else {\n updateConversationId(null);\n }\n };\n\n const handleSwitchConversation = (id: string) => {\n if (!isLegacy && id === conversationIdRef.current) return;\n // Reuse the full new-chat reset (abort in-flight request + uploads,\n // clear transcript/composer/status), then adopt the selected id and\n // re-arm the history-restore effect so the host panel fetches the\n // conversation's messages via the sessions endpoint.\n handleNewChat();\n if (!isLegacy) {\n updateConversationId(id);\n }\n };\n\n const handleStopGenerating = () => {\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n setIsLoading(false);\n setAgentStatus(null);\n hasUsedToolsRef.current = false;\n setMessages((prev) => prev.filter((m) => !(m.role === 'assistant' && !m.content)));\n };\n\n // Steering affordances are only advertised when the typed text can actually\n // be dispatched mid-run: a response is streaming (isLoading), the REST steer\n // endpoint is configured, and the conversation already has a server id (the\n // very first turn of a fresh conversation only receives its id on `done`).\n const canSteer = isLoading && getSteerUrl() !== null && conversationId !== null;\n\n return {\n messages,\n inputValue,\n setInputValue,\n isLoading,\n agentStatus,\n attachedFiles,\n conversationId,\n transferredAgent,\n canSteer,\n historyLoadedRef,\n conversationIdRef,\n handleFileAdd,\n handlePaste,\n handleSendMessage,\n handleNewChat,\n handleStopGenerating,\n setAttachedFiles,\n setMessages,\n updateConversationId,\n handleSwitchConversation,\n };\n}\n","import { useEffect, useState } from 'react';\nimport type { ApiEndpoints, BackendType, XtmAgent } from '../types';\n\nconst STORAGE_AGENT_KEY = 'filigranChatAgentSlug';\n\ninterface UseAgentsOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n requestHeaders?: Record<string, string>;\n}\n\ninterface UseAgentsReturn {\n agents: XtmAgent[];\n selectedAgent: XtmAgent | null;\n setSelectedAgent: React.Dispatch<React.SetStateAction<XtmAgent | null>>;\n agentMenuOpen: boolean;\n setAgentMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;\n handleSwitchAgent: (agent: XtmAgent, onSwitch?: () => void) => void;\n}\n\nexport function useAgents({ apiBaseUrl, apiEndpoints, backendType = 'rest', requestHeaders }: UseAgentsOptions): UseAgentsReturn {\n const [agents, setAgents] = useState<XtmAgent[]>([]);\n const [selectedAgent, setSelectedAgent] = useState<XtmAgent | null>(null);\n const [agentMenuOpen, setAgentMenuOpen] = useState(false);\n\n useEffect(() => {\n // Skip agents fetch if disabled, using single endpoint mode, or legacy backend\n if (apiEndpoints?.agents === null || apiEndpoints?.singleEndpoint || backendType === 'legacy') {\n return;\n }\n const agentsUrl = `${apiBaseUrl}${apiEndpoints?.agents ?? '/chat/agents'}`;\n fetch(agentsUrl, { headers: requestHeaders })\n .then((res) => (res.ok ? res.json() : []))\n .then((data: XtmAgent[]) => {\n setAgents(data);\n if (data.length > 0 && !selectedAgent) {\n const savedSlug = localStorage.getItem(STORAGE_AGENT_KEY);\n const match = savedSlug ? data.find((a) => a.slug === savedSlug) : null;\n setSelectedAgent(match || data[0]);\n }\n })\n .catch(() => {});\n }, [apiBaseUrl, apiEndpoints, backendType, requestHeaders]);\n\n const handleSwitchAgent = (agent: XtmAgent, onSwitch?: () => void) => {\n if (agent.id === selectedAgent?.id) {\n setAgentMenuOpen(false);\n return;\n }\n setSelectedAgent(agent);\n if (agent.slug) localStorage.setItem(STORAGE_AGENT_KEY, agent.slug);\n setAgentMenuOpen(false);\n onSwitch?.();\n };\n\n return {\n agents,\n selectedAgent,\n setSelectedAgent,\n agentMenuOpen,\n setAgentMenuOpen,\n handleSwitchAgent,\n };\n}\n","import { useCallback, useState } from 'react';\nimport type { ApiEndpoints, BackendType, ChatConversationSummary } from '../types';\n\ninterface UseConversationsOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n requestHeaders?: Record<string, string>;\n}\n\ninterface UseConversationsReturn {\n /** Whether the history feature is available at all (REST backend with a sessions endpoint). */\n historyEnabled: boolean;\n conversations: ChatConversationSummary[];\n conversationsLoading: boolean;\n /** Fetch (or re-fetch) the conversation list. No-ops when history is disabled. */\n refreshConversations: () => Promise<void>;\n /** Delete a conversation server-side. Returns true on success. */\n deleteConversation: (id: string) => Promise<boolean>;\n}\n\n/**\n * Normalize one raw conversation entry from the backend list response.\n * Defensive: skips entries without a conversation id, accepts both the\n * snake_case REST shape and a few aliases so older proxies keep working.\n */\nfunction parseConversation(raw: unknown): ChatConversationSummary | null {\n if (!raw || typeof raw !== 'object') return null;\n const c = raw as Record<string, unknown>;\n const id = c.conversation_id ?? c.id;\n if (typeof id !== 'string' || !id) return null;\n // Keep the raw (trimmed) title; the localized \"Untitled conversation\"\n // fallback is applied at render time (ChatHeader) so it goes through the\n // component's translation function instead of being hardcoded in English.\n const title = typeof c.title === 'string' ? c.title.trim() : '';\n const updatedAt = typeof c.updated_at === 'string' ? c.updated_at : typeof c.created_at === 'string' ? c.created_at : undefined;\n const messageCount = typeof c.message_count === 'number' ? c.message_count : undefined;\n return { conversationId: id, title, updatedAt, messageCount };\n}\n\n/**\n * Multi-conversation history for the REST backend (mirrors the XTM One web\n * chat sidebar). The conversation list is fetched lazily — when the history\n * menu opens — via `GET {apiBaseUrl}{sessions}`, and conversations are\n * deleted via `DELETE {apiBaseUrl}{sessions}/{conversation_id}`.\n *\n * Degrades gracefully: a backend that doesn't implement the list endpoint\n * yet (404/405/network error) simply yields an empty list, so the menu shows\n * its empty state instead of breaking the chat.\n */\nexport function useConversations({\n apiBaseUrl,\n apiEndpoints,\n backendType = 'rest',\n requestHeaders,\n}: UseConversationsOptions): UseConversationsReturn {\n const [conversations, setConversations] = useState<ChatConversationSummary[]>([]);\n const [conversationsLoading, setConversationsLoading] = useState(false);\n\n const historyEnabled = backendType === 'rest' && !apiEndpoints?.singleEndpoint && apiEndpoints?.sessions !== null && apiEndpoints?.history !== null;\n\n const sessionsUrl = `${apiBaseUrl}${apiEndpoints?.history ?? apiEndpoints?.sessions ?? '/chat/sessions'}`;\n\n const refreshConversations = useCallback(async () => {\n if (!historyEnabled) return;\n setConversationsLoading(true);\n try {\n const res = await fetch(sessionsUrl, {\n method: 'GET',\n headers: { ...(requestHeaders ?? {}) },\n });\n if (!res.ok) {\n setConversations([]);\n return;\n }\n const data: unknown = await res.json();\n const rawList = Array.isArray(data)\n ? data\n : Array.isArray((data as Record<string, unknown>)?.conversations)\n ? ((data as Record<string, unknown>).conversations as unknown[])\n : [];\n setConversations(rawList.map(parseConversation).filter((c): c is ChatConversationSummary => c !== null));\n } catch {\n setConversations([]);\n } finally {\n setConversationsLoading(false);\n }\n }, [historyEnabled, sessionsUrl, requestHeaders]);\n\n const deleteConversation = useCallback(\n async (id: string): Promise<boolean> => {\n if (!historyEnabled) return false;\n try {\n const res = await fetch(`${sessionsUrl}/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n headers: { ...(requestHeaders ?? {}) },\n });\n if (!res.ok) return false;\n setConversations((prev) => prev.filter((c) => c.conversationId !== id));\n return true;\n } catch {\n return false;\n }\n },\n [historyEnabled, sessionsUrl, requestHeaders],\n );\n\n return { historyEnabled, conversations, conversationsLoading, refreshConversations, deleteConversation };\n}\n","import { useEffect, useRef, useState } from 'react';\nimport type { ChatMode } from '../types';\n\nconst SIDEBAR_WIDTH = 400;\nconst SIDEBAR_WIDTH_STORAGE_KEY = 'filigranChatSidebarWidth';\nconst MAX_SIDEBAR_RATIO = 0.4;\n\ninterface UseSidebarResizeOptions {\n mode: ChatMode;\n resizable: boolean;\n onWidthChange?: (width: number) => void;\n onResizeStart?: () => void;\n onResizeEnd?: () => void;\n}\n\ninterface UseSidebarResizeReturn {\n sidebarWidth: number;\n handleResizeStart: (e: React.MouseEvent) => void;\n defaultWidth: number;\n isResizing: boolean;\n}\n\nexport function useSidebarResize({ mode, resizable, onWidthChange, onResizeStart, onResizeEnd }: UseSidebarResizeOptions): UseSidebarResizeReturn {\n const [sidebarWidth, setSidebarWidth] = useState<number>(() => {\n if (typeof window === 'undefined') return SIDEBAR_WIDTH;\n const stored = localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY);\n if (stored) {\n const parsed = parseInt(stored, 10);\n if (!Number.isNaN(parsed) && parsed >= SIDEBAR_WIDTH) return parsed;\n }\n return SIDEBAR_WIDTH;\n });\n const [isResizing, setIsResizing] = useState(false);\n\n const isResizingRef = useRef(false);\n const sidebarWidthRef = useRef(sidebarWidth);\n sidebarWidthRef.current = sidebarWidth;\n const onWidthChangeRef = useRef(onWidthChange);\n onWidthChangeRef.current = onWidthChange;\n const onResizeEndRef = useRef(onResizeEnd);\n onResizeEndRef.current = onResizeEnd;\n\n // Notify parent of sidebar width when entering sidebar mode\n useEffect(() => {\n if (mode === 'sidebar' && resizable) {\n onWidthChangeRef.current?.(sidebarWidthRef.current);\n }\n }, [mode, resizable]);\n\n // Resize event handlers\n useEffect(() => {\n if (mode !== 'sidebar' || !resizable) return undefined;\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isResizingRef.current) return;\n e.preventDefault();\n const newWidth = window.innerWidth - e.clientX;\n const maxWidth = window.innerWidth * MAX_SIDEBAR_RATIO;\n const clamped = Math.min(Math.max(newWidth, SIDEBAR_WIDTH), maxWidth);\n setSidebarWidth(clamped);\n sidebarWidthRef.current = clamped;\n onWidthChangeRef.current?.(clamped);\n };\n\n const handleMouseUp = () => {\n if (!isResizingRef.current) return;\n isResizingRef.current = false;\n setIsResizing(false);\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(sidebarWidthRef.current));\n onResizeEndRef.current?.();\n };\n\n const handleWindowResize = () => {\n const maxWidth = window.innerWidth * MAX_SIDEBAR_RATIO;\n if (sidebarWidthRef.current > maxWidth) {\n const clamped = Math.max(maxWidth, SIDEBAR_WIDTH);\n setSidebarWidth(clamped);\n sidebarWidthRef.current = clamped;\n onWidthChangeRef.current?.(clamped);\n }\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n window.addEventListener('resize', handleWindowResize);\n\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n window.removeEventListener('resize', handleWindowResize);\n };\n }, [mode, resizable]);\n\n const handleResizeStart = (e: React.MouseEvent) => {\n e.preventDefault();\n isResizingRef.current = true;\n setIsResizing(true);\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n onResizeStart?.();\n };\n\n return {\n sidebarWidth,\n handleResizeStart,\n defaultWidth: SIDEBAR_WIDTH,\n isResizing,\n };\n}\n","import { useEffect, useRef } from 'react';\n\n/**\n * A turn must run at least this long before its completion is worth a\n * notification — instant replies never raise one.\n */\nconst MIN_NOTICE_MS = 4000;\n/** Document-title flash cadence while the user is away. */\nconst TITLE_FLASH_MS = 1200;\n\n// The document title is a single page-global resource, so the flash is shared\n// across hook instances on purpose (per-hook timers would fight over the one\n// `document.title` and clobber each other's saved title). `activeHooks` ref-\n// counts mounted, enabled instances so one panel unmounting never cancels a\n// flash another panel still owns — only the last one to leave restores it.\nlet flashTimer: number | null = null;\nlet originalTitle: string | null = null;\nlet activeHooks = 0;\n\n/** Stop flashing and restore the page title captured when the flash began. */\nfunction stopTitleFlash(): void {\n if (flashTimer !== null) {\n window.clearInterval(flashTimer);\n flashTimer = null;\n }\n if (originalTitle !== null) {\n document.title = originalTitle;\n originalTitle = null;\n }\n}\n\n/**\n * Flash the document title so a multitasking user notices the answer landed\n * even from another tab or window. The flash is stopped (and the title\n * restored) the moment the tab is visible AND focused again, by the\n * hook-scoped listeners below. Title flashing needs no permission and is the\n * reliable baseline of the completion notification.\n */\nfunction startTitleFlash(message: string): void {\n if (typeof document === 'undefined') return;\n if (originalTitle === null) originalTitle = document.title;\n if (flashTimer !== null) window.clearInterval(flashTimer);\n let showMessage = true;\n document.title = message;\n flashTimer = window.setInterval(() => {\n showMessage = !showMessage;\n document.title = showMessage ? message : (originalTitle ?? message);\n }, TITLE_FLASH_MS);\n}\n\n/**\n * Best-effort OS notification — only fired when the user has already granted\n * permission. We deliberately never call `Notification.requestPermission()`\n * unprompted: the title flash (and the host toast) cover the case where OS\n * notifications are unavailable.\n */\nfunction notifyOS(title: string, body: string): void {\n try {\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return;\n new Notification(title, { body, tag: 'filigran-chat-complete' });\n } catch {\n /* Notification constructor can throw on some platforms — ignore. */\n }\n}\n\ninterface UseAwayCompletionNoticeOptions {\n /** True while a response is being generated. */\n isLoading: boolean;\n /** Name of the answering agent, used in the notification body. */\n agentName: string;\n t: (key: string) => string;\n /** Master switch (default true). */\n enabled?: boolean;\n /**\n * Host hook fired when a long turn finishes and the user is not actively\n * watching the chat — either away (tab hidden / another window) or in-app\n * with the chat surface closed/hidden (`isViewingChat` reports not-viewing).\n * Lets the host raise its own in-app toast (the chatbot has no toast surface\n * of its own). Receives the translated strings.\n */\n onComplete?: (title: string, body: string) => void;\n /**\n * Returns true when the chat surface is on screen for the user (the panel is\n * open and visible) — NOT merely whether focus sits inside it. When provided,\n * the notice also fires if the chat surface is hidden/closed while the tab is\n * still focused (e.g. a docked sidebar the host has collapsed). It must NOT\n * key on focus-within: in sidebar/floating mode the user reads a streamed\n * answer while their focus stays in the host app, and pinging them for an\n * answer they can already see is exactly the noise this guards against. When\n * omitted, only the away case (tab hidden / window unfocused) triggers it.\n */\n isViewingChat?: () => boolean;\n}\n\n/**\n * Notify the user when a long-running turn finishes and they are not watching\n * the chat. State is read at completion (never latched mid-turn) so someone who\n * stepped away but returned before the turn finished is not pinged:\n * - **Away** (tab hidden or another window/app focused): document-title flash +\n * OS notification (when granted) + host toast.\n * - **Surface not visible** (window focused & tab visible, but the chat panel\n * is closed/hidden — only when `isViewingChat` reports not-viewing): host\n * toast only.\n * - **Actively watching** (tab visible, window focused, panel on screen):\n * nothing — the streamed answer is itself the feedback.\n */\nexport function useAwayCompletionNotice({\n isLoading,\n agentName,\n t,\n enabled = true,\n onComplete,\n isViewingChat,\n}: UseAwayCompletionNoticeOptions): void {\n const wasLoadingRef = useRef(false);\n const startRef = useRef(0);\n\n // Stop the flash and restore the title as soon as the user returns to a\n // visible, focused tab. Bound only while enabled and torn down on unmount (or\n // when the host disables it), so we never leak listeners or leave the title\n // flashing after the panel is gone.\n useEffect(() => {\n if (!enabled || typeof document === 'undefined') return;\n activeHooks += 1;\n const clearOnReturn = () => {\n if (!document.hidden && document.hasFocus()) stopTitleFlash();\n };\n document.addEventListener('visibilitychange', clearOnReturn);\n window.addEventListener('focus', clearOnReturn);\n return () => {\n document.removeEventListener('visibilitychange', clearOnReturn);\n window.removeEventListener('focus', clearOnReturn);\n activeHooks = Math.max(0, activeHooks - 1);\n // Only restore the title once the last consumer leaves, so unmounting one\n // panel never cancels another panel's in-flight flash.\n if (activeHooks === 0) stopTitleFlash();\n };\n }, [enabled]);\n\n useEffect(() => {\n const wasLoading = wasLoadingRef.current;\n wasLoadingRef.current = isLoading;\n\n if (isLoading && !wasLoading) {\n startRef.current = Date.now();\n return;\n }\n\n if (!isLoading && wasLoading && enabled) {\n const elapsed = Date.now() - startRef.current;\n if (elapsed < MIN_NOTICE_MS) return;\n const hidden = typeof document !== 'undefined' && document.hidden;\n const unfocused = typeof document !== 'undefined' && !document.hasFocus();\n const away = hidden || unfocused;\n const viewingChat = isViewingChat ? isViewingChat() : true;\n // Focused tab + looking at the chat → the streamed answer is the feedback.\n if (!away && viewingChat) return;\n const title = t('Response ready');\n const body = agentName ? `${agentName} ${t('has finished')}` : t('Your answer is ready');\n if (away) {\n startTitleFlash(title);\n notifyOS(title, body);\n }\n onComplete?.(title, body);\n }\n }, [isLoading, enabled, agentName, t, onComplete, isViewingChat]);\n}\n","import type { IconProps } from '../../types';\n\nexport const AlertTriangleIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" />\n <path d=\"M12 9v4\" />\n <path d=\"M12 17h.01\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ArrowRightLeftIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m16 3 4 4-4 4\" />\n <path d=\"M20 7H4\" />\n <path d=\"m8 21-4-4 4-4\" />\n <path d=\"M4 17h16\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const AttachFileIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const BotIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M12 8V4H8\" />\n <rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\" />\n <path d=\"M2 14h2\" />\n <path d=\"M20 14h2\" />\n <path d=\"M15 13v2\" />\n <path d=\"M9 13v2\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const BrainIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z\" />\n <path d=\"M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z\" />\n <path d=\"M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4\" />\n <path d=\"M17.599 6.5a3 3 0 0 0 .399-1.375\" />\n <path d=\"M6.003 5.125A3 3 0 0 0 6.401 6.5\" />\n <path d=\"M3.477 10.896a4 4 0 0 1 .585-.396\" />\n <path d=\"M19.938 10.5a4 4 0 0 1 .585.396\" />\n <path d=\"M6 18a4 4 0 0 1-1.967-.516\" />\n <path d=\"M19.967 17.484A4 4 0 0 1 18 18\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const CheckCircleIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"m9 12 2 2 4-4\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const CheckIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ChevronDownIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const CloseIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const CopyIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const DatabaseIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\" />\n <path d=\"M3 5V19A9 3 0 0 0 21 19V5\" />\n <path d=\"M3 12A9 3 0 0 0 21 12\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const DefaultLogoIcon = ({ className, size = 24 }: IconProps) => (\n <svg xmlns=\"http://www.w3.org/2000/svg\" width={size} height={size} viewBox=\"0 0 24 24\" fill=\"currentColor\" stroke=\"none\" className={className}>\n <path d=\"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const DownloadIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n <polyline points=\"7 10 12 15 17 10\" />\n <line x1=\"12\" x2=\"12\" y1=\"15\" y2=\"3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const EditIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M12 20h9\" />\n <path d=\"M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ExternalLinkIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M15 3h6v6\" />\n <path d=\"M10 14 21 3\" />\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FileIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" />\n <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FloatingIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M11 13H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7\" />\n <rect width=\"12\" height=\"12\" x=\"10\" y=\"10\" rx=\"2\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FullscreenExitIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3\" />\n <path d=\"M21 8h-3a2 2 0 0 1-2-2V3\" />\n <path d=\"M3 16h3a2 2 0 0 0 2 2v3\" />\n <path d=\"M16 21v-3a2 2 0 0 1 2-2h3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FullscreenIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3\" />\n <path d=\"M21 8V5a2 2 0 0 0-2-2h-3\" />\n <path d=\"M3 16v3a2 2 0 0 0 2 2h3\" />\n <path d=\"M16 21h3a2 2 0 0 0 2-2v-3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const GamepadIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <line x1=\"6\" x2=\"10\" y1=\"11\" y2=\"11\" />\n <line x1=\"8\" x2=\"8\" y1=\"9\" y2=\"13\" />\n <line x1=\"15\" x2=\"15.01\" y1=\"12\" y2=\"12\" />\n <line x1=\"18\" x2=\"18.01\" y1=\"10\" y2=\"10\" />\n <path d=\"M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const GlobeIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\" />\n <path d=\"M2 12h20\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const HistoryIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n <path d=\"M12 7v5l4 2\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const InfoIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 16v-4\" />\n <path d=\"M12 8h.01\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const MailIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\" />\n <path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const SearchIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"m21 21-4.3-4.3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const SendIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m22 2-7 20-4-9-9-4Z\" />\n <path d=\"m22 2-11 11\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const SidebarIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" />\n <path d=\"M15 3v18\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const SparklesIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const StopCircleIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <rect width=\"6\" height=\"6\" x=\"9\" y=\"9\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const TerminalIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <polyline points=\"4 17 10 11 4 5\" />\n <line x1=\"12\" x2=\"20\" y1=\"19\" y2=\"19\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const TrashIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M3 6h18\" />\n <path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\" />\n <path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\" />\n <line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\" />\n <line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const UserPlusIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <line x1=\"19\" x2=\"19\" y1=\"8\" y2=\"14\" />\n <line x1=\"22\" x2=\"16\" y1=\"11\" y2=\"11\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const WrenchIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const XCircleIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"m15 9-6 6\" />\n <path d=\"m9 9 6 6\" />\n </svg>\n);\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useClickOutside } from '../hooks/useClickOutside';\nimport { findChatbotRoot } from '../utils';\n\ninterface DropdownProps {\n open: boolean;\n onClose: () => void;\n anchorRef: React.RefObject<HTMLElement | null>;\n placement?: 'bottom-start' | 'bottom-end';\n width?: number;\n children: React.ReactNode;\n}\n\nexport const Dropdown = ({ open, onClose, anchorRef, placement = 'bottom-start', width = 280, children }: DropdownProps) => {\n const panelRef = useRef<HTMLDivElement>(null);\n const [pos, setPos] = useState({ top: 0, left: 0 });\n\n const stableOnClose = useCallback(() => onClose(), [onClose]);\n useClickOutside(panelRef, stableOnClose, open);\n\n useEffect(() => {\n if (!open || !anchorRef.current) return;\n const rect = anchorRef.current.getBoundingClientRect();\n const left = placement === 'bottom-end' ? rect.right - width : rect.left;\n setPos({ top: rect.bottom + 4, left });\n }, [open, anchorRef, placement, width]);\n\n if (!open) return null;\n\n const portalTarget = findChatbotRoot(anchorRef.current);\n\n return createPortal(\n <div\n ref={panelRef}\n className=\"fixed z-[10000] rounded-[10px] overflow-hidden border border-gray-200 dark:border-white/10 bg-white dark:bg-[#2a2a3e] shadow-xl\"\n style={{ top: pos.top, left: pos.left, width }}\n >\n {children}\n </div>,\n portalTarget,\n );\n};\n","import { useEffect, type RefObject } from 'react';\n\nexport function useClickOutside(ref: RefObject<HTMLElement | null>, handler: () => void, active = true) {\n useEffect(() => {\n if (!active) return undefined;\n const listener = (e: MouseEvent | TouchEvent) => {\n if (!ref.current || ref.current.contains(e.target as Node)) return;\n handler();\n };\n document.addEventListener('mousedown', listener);\n document.addEventListener('touchstart', listener);\n return () => {\n document.removeEventListener('mousedown', listener);\n document.removeEventListener('touchstart', listener);\n };\n }, [ref, handler, active]);\n}\n","interface SpinnerProps {\n size?: number;\n className?: string;\n}\n\nexport const Spinner = ({ size = 16, className = '' }: SpinnerProps) => (\n <div\n className={`animate-spin rounded-full border-2 border-current/20 border-t-[var(--chat-accent)] ${className}`}\n style={{ width: size, height: size }}\n />\n);\n","import { useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { findChatbotRoot } from '../utils';\n\ninterface TooltipProps {\n title: string;\n children: React.ReactElement;\n}\n\n// Approximate rendered tooltip height (text-xs + py-1) plus the 4px gap.\n// Used to decide whether a top-placed tooltip would overflow the panel.\nconst TOOLTIP_CLEARANCE = 28;\n\nexport const Tooltip = ({ title, children }: TooltipProps) => {\n const ref = useRef<HTMLSpanElement>(null);\n const [show, setShow] = useState(false);\n const [pos, setPos] = useState({ top: 0, left: 0 });\n const [below, setBelow] = useState(false);\n\n if (!title) return children;\n\n const handleEnter = () => {\n if (!ref.current) return;\n const rect = ref.current.getBoundingClientRect();\n // Flip below the anchor when a top-placed tooltip would extend above the\n // chatbot panel's top edge. The tooltip lives inside the panel's stacking\n // context (z-[1200] in sidebar mode), so anything drawn above the panel\n // lands in the host app's top-bar zone and is hidden whenever the host\n // bar stacks higher (e.g. OpenAEV's AppBar at theme.zIndex.drawer + 1).\n const rootTop = findChatbotRoot(ref.current).getBoundingClientRect().top;\n const flip = rect.top - rootTop < TOOLTIP_CLEARANCE;\n setBelow(flip);\n setPos({\n top: flip ? rect.bottom + 4 : rect.top - 4,\n left: rect.left + rect.width / 2,\n });\n setShow(true);\n };\n\n return (\n <span ref={ref} className=\"inline-flex\" onMouseEnter={handleEnter} onMouseLeave={() => setShow(false)}>\n {children}\n {show &&\n createPortal(\n <span\n className={`pointer-events-none fixed z-[10001] -translate-x-1/2 ${below ? '' : '-translate-y-full'} whitespace-nowrap rounded-md bg-gray-900 dark:bg-gray-100 px-2 py-1 text-xs text-white dark:text-gray-900 shadow-lg`}\n style={{ top: pos.top, left: pos.left }}\n role=\"tooltip\"\n >\n {title}\n </span>,\n findChatbotRoot(ref.current),\n )}\n </span>\n );\n};\n","import { useRef } from 'react';\nimport type { ChatConversationSummary, ChatMode, XtmAgent } from '../types';\nimport { timeAgo } from '../utils';\nimport {\n ChevronDownIcon,\n CloseIcon,\n EditIcon,\n ExternalLinkIcon,\n FloatingIcon,\n FullscreenExitIcon,\n FullscreenIcon,\n HistoryIcon,\n SidebarIcon,\n TrashIcon,\n UserPlusIcon,\n} from './icons';\nimport { Dropdown } from './Dropdown';\nimport { Spinner } from './Spinner';\nimport { Tooltip } from './Tooltip';\n\ninterface ChatHeaderProps {\n mode: ChatMode;\n agentName: string;\n agents: XtmAgent[];\n selectedAgent: XtmAgent | null;\n transferredFrom?: string;\n agentMenuOpen: boolean;\n onAgentMenuToggle: () => void;\n onAgentMenuClose: () => void;\n onSwitchAgent: (agent: XtmAgent) => void;\n modeMenuOpen: boolean;\n onModeMenuToggle: () => void;\n onModeMenuClose: () => void;\n onModeChange: (mode: ChatMode) => void;\n onNewChat: () => void;\n onClose: () => void;\n logoIcon: React.ReactNode;\n agentDashboardUrl?: string;\n /** Multi-conversation history menu (REST backend). Hidden when false. */\n historyEnabled?: boolean;\n historyMenuOpen?: boolean;\n onHistoryMenuToggle?: () => void;\n onHistoryMenuClose?: () => void;\n conversations?: ChatConversationSummary[];\n conversationsLoading?: boolean;\n activeConversationId?: string | null;\n onSelectConversation?: (id: string) => void;\n onDeleteConversation?: (id: string) => void;\n t: (key: string) => string;\n}\n\nconst modeOptions: { mode: ChatMode; label: string; getIcon: (p: { size: number; className: string }) => React.ReactNode }[] = [\n { mode: 'floating', label: 'Floating', getIcon: (p) => <FloatingIcon {...p} /> },\n { mode: 'sidebar', label: 'Sidebar', getIcon: (p) => <SidebarIcon {...p} /> },\n { mode: 'fullscreen', label: 'Full screen', getIcon: (p) => <FullscreenIcon {...p} /> },\n];\n\nexport const ChatHeader = ({\n mode,\n agentName,\n agents,\n selectedAgent,\n transferredFrom,\n agentMenuOpen,\n onAgentMenuToggle,\n onAgentMenuClose,\n onSwitchAgent,\n modeMenuOpen,\n onModeMenuToggle,\n onModeMenuClose,\n onModeChange,\n onNewChat,\n onClose,\n logoIcon,\n agentDashboardUrl,\n historyEnabled = false,\n historyMenuOpen = false,\n onHistoryMenuToggle,\n onHistoryMenuClose,\n conversations = [],\n conversationsLoading = false,\n activeConversationId = null,\n onSelectConversation,\n onDeleteConversation,\n t,\n}: ChatHeaderProps) => {\n const agentAnchorRef = useRef<HTMLButtonElement>(null);\n const modeAnchorRef = useRef<HTMLButtonElement>(null);\n const historyAnchorRef = useRef<HTMLButtonElement>(null);\n\n const CurrentModeIcon = mode === 'sidebar' ? SidebarIcon : mode === 'fullscreen' ? FullscreenExitIcon : FloatingIcon;\n\n return (\n <div\n className={`flex items-center px-3 py-2 min-h-[48px] border-b border-gray-200 dark:border-white/10 bg-gradient-to-br from-[var(--chat-accent-dark)]/[0.13] to-[var(--chat-accent)]/[0.07] ${mode === 'floating' ? 'rounded-t-xl' : ''}`}\n >\n <div className=\"min-w-0\">\n <button\n ref={agentAnchorRef}\n type=\"button\"\n onClick={onAgentMenuToggle}\n className=\"flex items-center gap-1.5 text-sm font-semibold text-gray-900 dark:text-white px-2 py-1 rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <span className=\"flex items-center text-[var(--chat-accent)] [&>svg]:w-[18px] [&>svg]:h-[18px]\">{logoIcon}</span>\n <span>{agentName}</span>\n <ChevronDownIcon size={16} className=\"text-gray-400 dark:text-white/30\" />\n </button>\n {transferredFrom && (\n <div className=\"pl-10 pr-2 text-[0.6rem] font-normal text-gray-400 dark:text-white/30\">\n {t('Transferred from')} {transferredFrom}\n </div>\n )}\n </div>\n\n <Dropdown open={agentMenuOpen} onClose={onAgentMenuClose} anchorRef={agentAnchorRef} width={280}>\n <span className=\"block px-4 pt-3 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">\n {t('Switch to another agent')}\n </span>\n {agents.length === 0 && (\n <div className=\"px-4 py-2\">\n <Spinner size={16} />\n </div>\n )}\n <div>\n {agents.map((agent) => (\n <button\n key={agent.id}\n type=\"button\"\n onClick={() => onSwitchAgent(agent)}\n className={`w-full flex items-center gap-2 px-4 py-1.5 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors ${\n agent.id === selectedAgent?.id ? 'bg-[var(--chat-accent)]/10' : ''\n }`}\n >\n <div className=\"w-7 h-7 rounded-full flex items-center justify-center shrink-0 bg-gradient-to-br from-[var(--chat-accent)]/20 to-[var(--chat-accent)]/5\">\n <span className=\"text-[var(--chat-accent)] [&>svg]:w-4 [&>svg]:h-4\">{logoIcon}</span>\n </div>\n <div className=\"min-w-0\">\n <div className=\"text-[0.8125rem] font-medium text-gray-900 dark:text-white truncate\">{agent.name}</div>\n {agent.description && <div className=\"text-[0.7rem] text-gray-500 dark:text-white/40 truncate\">{agent.description}</div>}\n </div>\n </button>\n ))}\n </div>\n <div className=\"h-px bg-gray-200 dark:bg-white/10 mx-2\" />\n <div>\n {agentDashboardUrl && (\n <button\n type=\"button\"\n onClick={() => {\n onAgentMenuClose();\n window.open(`${agentDashboardUrl}/agents`, '_blank');\n }}\n className=\"w-full flex items-center gap-2 px-4 py-1.5 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <ExternalLinkIcon size={18} className=\"text-gray-400 dark:text-white/40 shrink-0\" />\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/70\">{t('Browse agents')}</span>\n </button>\n )}\n {agentDashboardUrl && (\n <button\n type=\"button\"\n onClick={() => {\n onAgentMenuClose();\n window.open(`${agentDashboardUrl}/agents/new`, '_blank');\n }}\n className=\"w-full flex items-center gap-2 px-4 py-1.5 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <UserPlusIcon size={18} className=\"text-gray-400 dark:text-white/40 shrink-0\" />\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/70\">{t('Create agent')}</span>\n </button>\n )}\n </div>\n </Dropdown>\n\n <div className=\"flex-1\" />\n\n {historyEnabled && (\n <>\n <Tooltip title={t('Conversation history')}>\n <button\n ref={historyAnchorRef}\n type=\"button\"\n onClick={onHistoryMenuToggle}\n aria-label={t('Conversation history')}\n aria-haspopup=\"menu\"\n aria-expanded={historyMenuOpen}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <HistoryIcon size={18} />\n </button>\n </Tooltip>\n\n <Dropdown open={historyMenuOpen} onClose={() => onHistoryMenuClose?.()} anchorRef={historyAnchorRef} placement=\"bottom-end\" width={300}>\n <span className=\"block px-4 pt-3 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">\n {t('Conversation history')}\n </span>\n <div className=\"max-h-72 overflow-y-auto filigran-chat-scrollable\">\n {conversationsLoading && conversations.length === 0 && (\n <div className=\"px-4 py-2\">\n <Spinner size={16} />\n </div>\n )}\n {!conversationsLoading && conversations.length === 0 && (\n <div className=\"px-4 py-3 text-[0.75rem] text-gray-400 dark:text-white/40\">{t('No conversations yet')}</div>\n )}\n {conversations.map((conv) => {\n const isActive = conv.conversationId === activeConversationId;\n const when = timeAgo(conv.updatedAt, t);\n return (\n <div\n key={conv.conversationId}\n className={`group flex items-center gap-2 px-4 py-1.5 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors ${\n isActive ? 'bg-[var(--chat-accent)]/10' : ''\n }`}\n >\n <button type=\"button\" onClick={() => onSelectConversation?.(conv.conversationId)} className=\"flex-1 min-w-0 text-left\">\n <div className=\"text-[0.8125rem] font-medium text-gray-900 dark:text-white truncate\">\n {conv.title || t('Untitled conversation')}\n </div>\n {when && <div className=\"text-[0.7rem] text-gray-500 dark:text-white/40 truncate\">{when}</div>}\n </button>\n {onDeleteConversation && (\n <button\n type=\"button\"\n onClick={() => onDeleteConversation(conv.conversationId)}\n title={t('Delete conversation')}\n aria-label={t('Delete conversation')}\n className=\"shrink-0 p-1 rounded-md text-gray-400 dark:text-white/30 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:text-red-500 dark:hover:text-red-400 transition-all\"\n >\n <TrashIcon size={14} />\n </button>\n )}\n </div>\n );\n })}\n </div>\n <div className=\"h-px bg-gray-200 dark:bg-white/10 mx-2\" />\n <button\n type=\"button\"\n onClick={() => {\n onHistoryMenuClose?.();\n onNewChat();\n }}\n className=\"w-full flex items-center gap-2 px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <EditIcon size={16} className=\"text-gray-400 dark:text-white/40 shrink-0\" />\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/70\">{t('New conversation')}</span>\n </button>\n </Dropdown>\n </>\n )}\n\n <Tooltip title={t('New chat')}>\n <button\n type=\"button\"\n onClick={onNewChat}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <EditIcon size={18} />\n </button>\n </Tooltip>\n\n <Tooltip title={t('Switch view')}>\n <button\n ref={modeAnchorRef}\n type=\"button\"\n onClick={onModeMenuToggle}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <CurrentModeIcon size={18} />\n </button>\n </Tooltip>\n\n <Dropdown open={modeMenuOpen} onClose={onModeMenuClose} anchorRef={modeAnchorRef} placement=\"bottom-end\" width={180}>\n <span className=\"block px-4 pt-3 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">{t('Switch to')}</span>\n <div className=\"pb-1\">\n {modeOptions.map((opt) => (\n <button\n key={opt.mode}\n type=\"button\"\n onClick={() => {\n onModeChange(opt.mode);\n onModeMenuClose();\n }}\n className={`w-full flex items-center gap-3 px-4 py-1 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors ${\n mode === opt.mode ? 'bg-[var(--chat-accent)]/10' : ''\n }`}\n >\n {opt.getIcon({ size: 18, className: 'text-gray-400 dark:text-white/40' })}\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/70\">{t(opt.label)}</span>\n </button>\n ))}\n </div>\n </Dropdown>\n\n <Tooltip title={t('Close')}>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <CloseIcon size={18} />\n </button>\n </Tooltip>\n </div>\n );\n};\n","import { useRef, type KeyboardEvent } from 'react';\nimport type { ChatFile, ChatMode } from '../types';\nimport { AttachFileIcon, FileIcon, SendIcon, StopCircleIcon } from './icons';\nimport { Tooltip } from './Tooltip';\n\ninterface ChatInputProps {\n inputValue: string;\n onInputChange: (value: string) => void;\n onSend: () => void;\n onStop: () => void;\n isLoading: boolean;\n /**\n * Mid-run steering availability: while the agent is generating, the typed\n * text can be dispatched immediately (Enter / accent Send button) and is\n * injected into the running run instead of waiting for it to finish.\n * Attachments keep the legacy wait behavior.\n */\n canSteer?: boolean;\n attachedFiles?: ChatFile[];\n onFileAdd?: (files: FileList | null) => void;\n onFileRemove?: (index: number) => void;\n onPaste?: (e: React.ClipboardEvent) => void;\n t: (key: string) => string;\n mode?: ChatMode;\n separatorColor?: string;\n}\n\nexport const ChatInput = ({\n inputValue,\n onInputChange,\n onSend,\n onStop,\n isLoading,\n canSteer = false,\n attachedFiles = [],\n onFileAdd,\n onFileRemove,\n onPaste,\n t,\n mode,\n separatorColor,\n}: ChatInputProps) => {\n const fileInputRef = useRef<HTMLInputElement>(null);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n onSend();\n }\n if (e.key === 'Escape' && isLoading) {\n e.preventDefault();\n onStop();\n }\n };\n\n const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n onInputChange(e.target.value);\n const el = e.target;\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, 120)}px`;\n };\n\n const isFileManagementEnabled = Boolean(onFileAdd && onFileRemove && onPaste);\n const hasContent = inputValue.trim() || (isFileManagementEnabled && attachedFiles.length > 0);\n const hasFilesUploading = isFileManagementEnabled && attachedFiles.some((f) => f.uploadStatus === 'pending');\n const canSend = hasContent && !hasFilesUploading;\n const hasAttachments = isFileManagementEnabled && attachedFiles.length > 0;\n // Show the accent Send button NEXT to Stop while generating: text-only\n // sends can steer the running agent. With attachments selected the send\n // must wait for the current response, so only Stop is shown.\n const showSteerSend = isLoading && canSteer && Boolean(inputValue.trim()) && !hasAttachments;\n\n const footerText =\n isLoading && canSteer && !hasAttachments\n ? t('Enter to send now · Esc to stop')\n : isLoading && hasAttachments\n ? t('Attachments wait for the current response')\n : t('Uses AI. Verify results.');\n\n return (\n <div\n className={`px-4 py-3 border-t border-gray-200 dark:border-white/10 ${mode === 'floating' ? 'rounded-b-xl' : ''}`}\n style={separatorColor ? { borderTopColor: separatorColor, borderTopWidth: 1 } : undefined}\n >\n {isFileManagementEnabled && attachedFiles.length > 0 && (\n <div className=\"flex gap-1.5 flex-wrap mb-2\">\n {attachedFiles.map((f, i) => (\n <span\n key={i}\n className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[0.7rem] ${\n f.uploadStatus === 'error'\n ? 'border-red-300 dark:border-red-500/30 text-red-500 dark:text-red-400'\n : f.uploadStatus === 'pending'\n ? 'border-gray-200 dark:border-white/10 text-gray-400 dark:text-white/40'\n : 'border-gray-200 dark:border-white/10 text-gray-600 dark:text-white/60'\n }`}\n >\n {f.uploadStatus === 'pending' ? (\n <span className=\"w-3.5 h-3.5 border border-current/30 border-t-current rounded-full animate-spin\" />\n ) : (\n <FileIcon size={14} />\n )}\n {f.name}\n {f.uploadStatus === 'error' && <span className=\"text-red-400 text-[0.6rem]\">✕</span>}\n <button\n type=\"button\"\n onClick={() => onFileRemove?.(i)}\n className=\"ml-0.5 text-gray-400 dark:text-white/30 hover:text-gray-600 dark:hover:text-white/60\"\n >\n ×\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div className=\"flex items-center border border-gray-200 dark:border-white/10 rounded-xl px-2 py-1 transition-colors focus-within:border-[var(--chat-accent)]\">\n {isFileManagementEnabled && (\n <>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n hidden\n onChange={(e) => {\n onFileAdd?.(e.target.files);\n e.target.value = '';\n }}\n />\n <button\n type=\"button\"\n onClick={() => fileInputRef.current?.click()}\n className=\"w-8 h-8 flex items-center justify-center shrink-0 rounded-lg text-gray-400 dark:text-white/30 hover:bg-gray-100 dark:hover:bg-white/10 mr-0.5 transition-colors\"\n >\n <AttachFileIcon size={18} />\n </button>\n </>\n )}\n <textarea\n ref={textareaRef}\n placeholder={t('Ask a question...')}\n value={inputValue}\n onChange={handleInput}\n onKeyDown={handleKeyDown}\n onPaste={onPaste}\n rows={1}\n className=\"flex-1 bg-transparent border-none outline-hidden resize-none text-[0.8125rem] py-1.5 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-white/30 filigran-chat-scrollable\"\n style={{ maxHeight: 120 }}\n />\n {showSteerSend && (\n <Tooltip title={t('Send now')}>\n <button\n type=\"button\"\n onClick={onSend}\n aria-label={t('Send now')}\n className=\"p-1.5 rounded-lg w-8 h-8 flex items-center justify-center transition-all duration-150 text-[var(--chat-accent)] bg-[var(--chat-accent)]/10 hover:bg-[var(--chat-accent)]/20\"\n >\n <SendIcon size={18} />\n </button>\n </Tooltip>\n )}\n <Tooltip title={isLoading ? t('Stop generating') : hasFilesUploading ? t('Files uploading...') : ''}>\n <button\n type=\"button\"\n onClick={isLoading ? onStop : onSend}\n disabled={!isLoading && !canSend}\n className={`p-1.5 rounded-lg w-8 h-8 flex items-center justify-center transition-all duration-150 ${\n isLoading\n ? 'text-red-500 bg-red-500/10 hover:bg-red-500/20 ml-0.5'\n : canSend\n ? 'text-[var(--chat-accent)] bg-[var(--chat-accent)]/10 hover:bg-[var(--chat-accent)]/20'\n : 'text-gray-300 dark:text-white/20 cursor-not-allowed'\n }`}\n >\n {isLoading ? <StopCircleIcon size={18} /> : <SendIcon size={18} />}\n </button>\n </Tooltip>\n </div>\n\n <p className=\"text-center text-[0.65rem] text-gray-400 dark:text-white/30 mt-1.5 opacity-70\">{footerText}</p>\n </div>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport { GamepadIcon } from './icons';\n\n/**\n * Playful, rotating \"still working on it\" messages shown below the status\n * bubble during longer waits. They double as the Space Invader's targets:\n * the little invader ship erases them letter by letter, then the next one\n * fades in. Kept short and upbeat so they fit on one line in the narrow\n * floating panel and read as a sense of progress rather than noise.\n */\nconst DEFAULT_MESSAGES = [\n 'Crunching the data',\n 'Connecting the dots',\n 'Consulting the sources',\n 'Thinking it through',\n 'Reticulating splines',\n 'Analyzing the details',\n 'Almost there',\n 'Putting it together',\n 'Polishing the answer',\n 'Wrapping things up',\n];\n\n/** localStorage key for the per-browser mini-game preference (on by default). */\nconst PREF_KEY = 'filigranChatMiniGame';\n\n/** Plain (no-game) message rotation cadence. */\nconst PLAIN_ROTATE_MS = 2600;\n\n/** Canvas height in CSS px — room for the target row + the gliding ship. */\nconst GAME_HEIGHT = 70;\n\n/** Chunky monospace for the retro arcade feel + even letter spacing. */\nconst arcadeFont = (px: number): string => `700 ${px}px 'Courier New', ui-monospace, monospace`;\n\n/**\n * Classic 11x8 \"crab\" invader, two leg frames toggled while it glides — the\n * silhouette reads instantly as a Space Invader. `1` = filled pixel.\n */\nconst INVADER_FRAMES: string[][] = [\n ['00100000100', '00010001000', '00111111100', '01101110110', '11111111111', '10111111101', '10100000101', '00011011000'],\n ['00100000100', '10010001001', '10111111101', '11101110111', '11111111111', '00111111100', '00100000100', '01000000010'],\n];\n\nfunction readPref(): boolean {\n if (typeof window === 'undefined') return true;\n try {\n return window.localStorage.getItem(PREF_KEY) !== 'off';\n } catch {\n return true;\n }\n}\n\nfunction writePref(on: boolean): void {\n try {\n window.localStorage.setItem(PREF_KEY, on ? 'on' : 'off');\n } catch {\n /* private mode / disabled storage — fall back to in-memory state only */\n }\n}\n\nfunction usePrefersReducedMotion(): boolean {\n const [reduced, setReduced] = useState(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n });\n useEffect(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return;\n const mql = window.matchMedia('(prefers-reduced-motion: reduce)');\n const onChange = () => setReduced(mql.matches);\n // Older Safari/WebKit only expose the deprecated addListener/removeListener.\n if (typeof mql.addEventListener === 'function') {\n mql.addEventListener('change', onChange);\n return () => mql.removeEventListener('change', onChange);\n }\n mql.addListener(onChange);\n return () => mql.removeListener(onChange);\n }, []);\n return reduced;\n}\n\n/** Px-from-bottom within which the user is considered \"still following\". */\nconst FOLLOW_THRESHOLD_PX = 140;\n\n/** Nearest vertically-scrollable ancestor of `el`, or null. */\nfunction findScrollParent(el: HTMLElement | null): HTMLElement | null {\n let node = el?.parentElement ?? null;\n while (node) {\n const oy = getComputedStyle(node).overflowY;\n if ((oy === 'auto' || oy === 'scroll') && node.scrollHeight > node.clientHeight) {\n return node;\n }\n node = node.parentElement;\n }\n return null;\n}\n\ninterface Letter {\n char: string;\n x: number;\n w: number;\n alive: boolean;\n}\n\ninterface Bullet {\n x: number;\n y: number;\n}\n\ninterface Particle {\n x: number;\n y: number;\n vx: number;\n vy: number;\n life: number;\n}\n\n/**\n * Self-contained canvas mini-game. Owns its own rAF loop, resize handling and\n * lifecycle; the only thing it reports back is the index of the message it is\n * currently destroying, so the host can keep an accessible text mirror in sync.\n * Returns a teardown function.\n */\nfunction createInvaderGame(canvas: HTMLCanvasElement, messages: string[], onMessage: (index: number) => void): () => void {\n const maybeCtx = canvas.getContext('2d');\n if (!maybeCtx) return () => {};\n const ctx: CanvasRenderingContext2D = maybeCtx;\n\n let raf = 0;\n let cssW = 0;\n const cssH = GAME_HEIGHT;\n let fontPx = 13;\n let accent = '#7b5cff';\n\n let msgIndex = 0;\n let letters: Letter[] = [];\n let targetIndex = -1;\n let bullets: Bullet[] = [];\n let particles: Particle[] = [];\n let shipX = 0;\n let cooldown = 0;\n let clearedAt = 0;\n let legFrame = 0;\n let legTimer = 0;\n let last = performance.now();\n\n const PX = 2;\n const SPRITE_W = 11 * PX;\n const SPRITE_H = 8 * PX;\n const shipTop = cssH - SPRITE_H - 4;\n const letterY = Math.round(cssH * 0.42);\n\n function resolveAccent(): void {\n const v = getComputedStyle(canvas).getPropertyValue('--chat-accent').trim();\n if (v) accent = v;\n }\n\n function firstAlive(from: number): number {\n for (let i = from; i < letters.length; i++) {\n if (letters[i].alive) return i;\n }\n return -1;\n }\n\n function layout(): void {\n if (cssW <= 0) return;\n const text = messages[msgIndex % messages.length] || '';\n onMessage(msgIndex % messages.length);\n\n // Shrink the font until the message fits the available width.\n fontPx = 13;\n for (; fontPx >= 8; fontPx--) {\n ctx.font = arcadeFont(fontPx);\n if (ctx.measureText(text).width <= cssW - 16) break;\n }\n ctx.font = arcadeFont(fontPx);\n\n const widths = Array.from(text).map((ch) => ctx.measureText(ch).width);\n const total = widths.reduce((a, b) => a + b, 0);\n let x = (cssW - total) / 2;\n letters = Array.from(text).map((ch, i) => {\n const lx = x;\n x += widths[i];\n return { char: ch, x: lx, w: widths[i], alive: ch.trim().length > 0 };\n });\n targetIndex = firstAlive(0);\n bullets = [];\n particles = [];\n cooldown = 0;\n clearedAt = 0;\n if (shipX <= 0) shipX = cssW / 2;\n }\n\n function letterCenter(i: number): number {\n return letters[i].x + letters[i].w / 2;\n }\n\n function update(dt: number): void {\n const dtf = Math.min(dt / 16.6667, 3);\n\n legTimer += dt;\n if (legTimer > 320) {\n legTimer = 0;\n legFrame ^= 1;\n }\n\n if (targetIndex === -1) {\n // Message fully cleared — brief pause, then load the next one.\n if (clearedAt === 0) clearedAt = performance.now();\n else if (performance.now() - clearedAt > 650) {\n msgIndex++;\n layout();\n }\n } else {\n const targetX = letterCenter(targetIndex);\n shipX += (targetX - shipX) * Math.min(0.16 * dtf, 1);\n cooldown -= dt;\n if (bullets.length === 0 && cooldown <= 0 && Math.abs(shipX - targetX) < 4) {\n bullets.push({ x: shipX, y: shipTop });\n cooldown = 130;\n }\n }\n\n for (let i = bullets.length - 1; i >= 0; i--) {\n bullets[i].y -= 2.6 * dtf;\n if (bullets[i].y <= letterY) {\n // Hit: detonate the current target letter and advance.\n if (targetIndex !== -1) {\n const cx = letterCenter(targetIndex);\n letters[targetIndex].alive = false;\n for (let p = 0; p < 7; p++) {\n const ang = (Math.PI * 2 * p) / 7 + Math.random();\n const spd = 0.6 + Math.random() * 1.4;\n particles.push({ x: cx, y: letterY, vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd, life: 1 });\n }\n targetIndex = firstAlive(targetIndex + 1);\n }\n bullets.splice(i, 1);\n }\n }\n\n for (let i = particles.length - 1; i >= 0; i--) {\n const p = particles[i];\n p.x += p.vx * dtf;\n p.y += p.vy * dtf;\n p.life -= 0.045 * dtf;\n if (p.life <= 0) particles.splice(i, 1);\n }\n }\n\n function draw(): void {\n ctx.clearRect(0, 0, cssW, cssH);\n\n // Single solid fill colour throughout; vary opacity via globalAlpha rather\n // than CSS color-mix() strings, which are not reliably accepted as a canvas\n // fillStyle on every browser (some fall back to opaque black).\n ctx.fillStyle = accent;\n\n ctx.font = arcadeFont(fontPx);\n ctx.textBaseline = 'middle';\n ctx.globalAlpha = 0.85;\n for (const l of letters) {\n if (l.alive) ctx.fillText(l.char, l.x, letterY);\n }\n\n ctx.globalAlpha = 1;\n for (const b of bullets) {\n ctx.fillRect(b.x - 1, b.y, 2, 7);\n }\n\n for (const p of particles) {\n ctx.globalAlpha = Math.max(p.life, 0) * 0.9;\n ctx.fillRect(p.x - 1.5, p.y - 1.5, 3, 3);\n }\n\n ctx.globalAlpha = 1;\n const frame = INVADER_FRAMES[legFrame];\n const ox = Math.round(shipX - SPRITE_W / 2);\n for (let r = 0; r < frame.length; r++) {\n const row = frame[r];\n for (let c = 0; c < row.length; c++) {\n if (row[c] === '1') ctx.fillRect(ox + c * PX, shipTop + r * PX, PX, PX);\n }\n }\n }\n\n function loop(now: number): void {\n const dt = now - last;\n last = now;\n if (!document.hidden && cssW > 0) {\n update(dt);\n draw();\n }\n raf = requestAnimationFrame(loop);\n }\n\n function applySize(): void {\n const rect = canvas.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n cssW = rect.width;\n canvas.width = Math.max(1, Math.round(cssW * dpr));\n canvas.height = Math.round(cssH * dpr);\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n resolveAccent();\n layout();\n }\n\n // Prefer ResizeObserver; fall back to a window resize listener on older\n // browsers / embedded webviews where it is unavailable (an unguarded\n // `new ResizeObserver(...)` would throw and break the waiting experience).\n let ro: ResizeObserver | null = null;\n if (typeof ResizeObserver !== 'undefined') {\n ro = new ResizeObserver(applySize);\n ro.observe(canvas);\n } else {\n window.addEventListener('resize', applySize);\n }\n applySize();\n raf = requestAnimationFrame(loop);\n\n return () => {\n cancelAnimationFrame(raf);\n if (ro) ro.disconnect();\n else window.removeEventListener('resize', applySize);\n };\n}\n\ninterface ChatWaitingGameProps {\n t: (key: string) => string;\n /** Host-level override; when false the feature is hidden entirely. */\n enabled?: boolean;\n}\n\n/**\n * Waiting experience shown below the status bubble during longer waits:\n * dynamic rotating messages with an optional Space Invader mini-game that\n * shoots the message letters away one by one. The game can be toggled off\n * per browser (preference persisted in localStorage); when off — or when the\n * OS requests reduced motion — the messages simply rotate as plain dimmed\n * text, so the \"dynamic loading messages\" feedback always stands on its own.\n */\nexport const ChatWaitingGame = ({ t, enabled = true }: ChatWaitingGameProps) => {\n const messages = useMemo(() => DEFAULT_MESSAGES.map((m) => t(m)), [t]);\n const reducedMotion = usePrefersReducedMotion();\n const [minigameOn, setMinigameOn] = useState(readPref);\n const [msgIndex, setMsgIndex] = useState(0);\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const rootRef = useRef<HTMLDivElement>(null);\n\n const playMode = enabled && minigameOn && !reducedMotion;\n\n // The game mounts below the last message but, unlike streamed reasoning/answer\n // text (which auto-scrolls on length change), nothing else triggers a scroll —\n // so at the bottom it lands under the fold. Reveal it on mount IF the user is\n // still following the bottom; if they scrolled up to read history, leave them.\n useEffect(() => {\n const scroller = findScrollParent(rootRef.current);\n if (!scroller) return;\n const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;\n if (distance <= FOLLOW_THRESHOLD_PX) {\n scroller.scrollTop = scroller.scrollHeight;\n }\n }, []);\n\n // Plain-text rotation when the game is off / reduced motion. Skipped while the\n // game drives the index, and entirely when the feature is disabled (so a\n // disabled host arms no timer); `enabled` is a dep so toggling it cleans up.\n useEffect(() => {\n if (playMode || !enabled) return;\n const id = window.setInterval(() => setMsgIndex((i) => (i + 1) % messages.length), PLAIN_ROTATE_MS);\n return () => window.clearInterval(id);\n }, [playMode, enabled, messages.length]);\n\n // Canvas engine when the game is on.\n useEffect(() => {\n if (!playMode) return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n return createInvaderGame(canvas, messages, setMsgIndex);\n }, [playMode, messages]);\n\n if (!enabled) return null;\n\n const current = messages[msgIndex % messages.length];\n\n return (\n <div ref={rootRef} className=\"ml-11 mt-2.5 max-w-[78%]\">\n {/* Keep the live region scoped to the announced text only — wrapping the\n whole UI (including the toggle button) made screen readers re-announce\n the control alongside each message change. */}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {current}\n </span>\n {/* Suppress every non-essential animation (fade-ins, the blinking caret)\n under prefers-reduced-motion, so the reduced-motion path really is\n motion-free — only the plain dimmed text rotates. */}\n <div\n className=\"relative overflow-hidden rounded-md bg-[var(--chat-accent)]/[0.03]\"\n style={reducedMotion ? undefined : { animation: 'chat-fade-in 0.5s ease-out' }}\n >\n {playMode ? (\n <canvas ref={canvasRef} aria-hidden className=\"block w-full\" style={{ height: GAME_HEIGHT }} />\n ) : (\n <div className=\"flex items-center\" style={{ height: GAME_HEIGHT }}>\n <span\n key={current}\n className=\"px-3 text-xs text-gray-500 dark:text-white/45\"\n style={reducedMotion ? undefined : { animation: 'chat-fade-in 0.4s ease-out' }}\n >\n {current}\n <span className={`ml-0.5 inline-block w-1 h-3 align-middle bg-[var(--chat-accent)]/60 ${reducedMotion ? '' : 'animate-pulse'}`} />\n </span>\n </div>\n )}\n {!reducedMotion && (\n <button\n type=\"button\"\n onClick={() => {\n const next = !minigameOn;\n setMinigameOn(next);\n writePref(next);\n }}\n aria-pressed={minigameOn}\n aria-label={minigameOn ? t('Turn off the waiting mini-game') : t('Turn on the waiting mini-game')}\n title={minigameOn ? t('Turn off the waiting mini-game') : t('Turn on the waiting mini-game')}\n className={`absolute top-1 right-1 rounded p-1 transition-opacity ${\n minigameOn ? 'text-[var(--chat-accent)] opacity-50 hover:opacity-100' : 'text-gray-400 dark:text-white/40 opacity-40 hover:opacity-90'\n }`}\n >\n <GamepadIcon size={13} />\n </button>\n )}\n </div>\n </div>\n );\n};\n","import { useEffect, useRef, useState } from 'react';\nimport type { AgentStatusState, IconProps } from '../types';\nimport {\n BrainIcon,\n DatabaseIcon,\n ExternalLinkIcon,\n GlobeIcon,\n MailIcon,\n SearchIcon,\n SparklesIcon,\n TerminalIcon,\n UserPlusIcon,\n WrenchIcon,\n} from './icons';\nimport { ChatWaitingGame } from './ChatWaitingGame';\n\ninterface ChatThinkingProps {\n agentStatus: AgentStatusState | null;\n logoIcon?: React.ReactNode;\n t: (key: string) => string;\n /** Host-level override for the waiting mini-game / dynamic messages. */\n miniGameEnabled?: boolean;\n}\n\ntype IconComponent = (props: IconProps) => React.JSX.Element;\n\ninterface StatusVisual {\n label: string;\n StatusIcon: IconComponent;\n showDots: boolean;\n}\n\nfunction resolveStatusVisual(agentStatus: AgentStatusState | null, t: (key: string) => string): StatusVisual {\n if (!agentStatus) {\n return { label: t('Thinking...'), StatusIcon: BrainIcon, showDots: false };\n }\n switch (agentStatus.status) {\n case 'tool_start': {\n const rawNames = agentStatus.tools ?? [];\n const lower = rawNames.map((n) => n.toLowerCase());\n\n // Delegation tools have dedicated statuses\n if (lower.some((n) => n === 'spawn_background_task')) {\n const count = rawNames.filter((n) => n === 'spawn_background_task').length;\n const label = count > 1 ? `${t('Delegating')} ${count} ${t('tasks')}…` : `${t('Delegating task')}…`;\n return { label, StatusIcon: UserPlusIcon, showDots: false };\n }\n if (lower.some((n) => n === 'check_task_status')) {\n const count = rawNames.filter((n) => n === 'check_task_status').length;\n const target = count > 1 ? `${count} ${t('background tasks')}` : t('background task');\n return { label: `${t('Waiting for')} ${target}…`, StatusIcon: SparklesIcon, showDots: false };\n }\n if (lower.some((n) => n === 'get_task_result')) {\n const count = rawNames.filter((n) => n === 'get_task_result').length;\n const from = count > 1 ? `${count} ${t('tasks')}` : t('task');\n return { label: `${t('Collecting results from')} ${from}…`, StatusIcon: SparklesIcon, showDots: false };\n }\n\n let StatusIcon: IconComponent = WrenchIcon;\n if (lower.some((n) => n.includes('search') || n.includes('list'))) {\n StatusIcon = SearchIcon;\n } else if (lower.some((n) => n.includes('read') || n.includes('get') || n.includes('query'))) {\n StatusIcon = DatabaseIcon;\n } else if (lower.some((n) => n.includes('send') || n.includes('create') || n.includes('draft') || n.includes('reply') || n.includes('flag'))) {\n StatusIcon = MailIcon;\n } else if (lower.some((n) => n.includes('code') || n.includes('execute'))) {\n StatusIcon = TerminalIcon;\n } else if (lower.some((n) => n.includes('web') || n.includes('browse'))) {\n StatusIcon = GlobeIcon;\n }\n let label: string;\n if (rawNames.length > 0) {\n const display = rawNames.map((n) => n.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase()));\n const unique = Array.from(new Set(display));\n label = unique.length === 1 ? `${unique[0]}…` : `${unique[0]} (+${unique.length - 1} more)…`;\n } else {\n label = t('Using tools…');\n }\n return { label, StatusIcon, showDots: false };\n }\n case 'analyzing':\n return { label: t('Analyzing results…'), StatusIcon: SparklesIcon, showDots: false };\n case 'steering':\n return { label: t('Incorporating your message…'), StatusIcon: SparklesIcon, showDots: true };\n case 'composing':\n return { label: t('Composing answer…'), StatusIcon: BrainIcon, showDots: true };\n case 'consulting': {\n const consultName = agentStatus.tools?.[0] ?? 'agent';\n return { label: `${t('Consulting')} ${consultName}…`, StatusIcon: UserPlusIcon, showDots: false };\n }\n case 'delegating': {\n const count = agentStatus.tools?.filter((n) => n === 'spawn_background_task').length ?? 0;\n return {\n label: count > 1 ? `${t('Delegating')} ${count} ${t('tasks')}…` : `${t('Delegating task')}…`,\n StatusIcon: UserPlusIcon,\n showDots: false,\n };\n }\n case 'polling': {\n const checkCount = agentStatus.tools?.filter((n) => n === 'check_task_status').length ?? 0;\n const target = checkCount > 1 ? `${checkCount} ${t('background tasks')}` : t('background task');\n return { label: `${t('Waiting for')} ${target}…`, StatusIcon: SparklesIcon, showDots: false };\n }\n case 'collecting': {\n const fetchCount = agentStatus.tools?.filter((n) => n === 'get_task_result').length ?? 0;\n const from = fetchCount > 1 ? `${fetchCount} ${t('tasks')}` : t('task');\n return { label: `${t('Collecting results from')} ${from}…`, StatusIcon: SparklesIcon, showDots: false };\n }\n case 'transferring': {\n const targetName = agentStatus.tools?.[0] ?? 'agent';\n return { label: `${t('Transferring to')} ${targetName}…`, StatusIcon: ExternalLinkIcon, showDots: false };\n }\n case 'thinking':\n default:\n return { label: t('Thinking...'), StatusIcon: BrainIcon, showDots: false };\n }\n}\n\n/**\n * Light markdown cleanup for reasoning prose. Preserves paragraph breaks so\n * multi-step reasoning stays readable inside the small scrolling window\n * instead of collapsing into one unbroken blob.\n */\nexport function cleanReasoningText(text: string): string {\n return text\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/`([^`]+)`/g, '$1')\n .replace(/\\*\\*(.+?)\\*\\*/g, '$1')\n .replace(/__(.+?)__/g, '$1')\n .replace(/#{1,6}\\s+/g, '')\n .replace(/^[ \\t]*[-*>]+[ \\t]*/gm, '')\n .replace(/[ \\t]+/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n/**\n * Reasoning window — the model's reasoning prose rendered below the status\n * bubble while the agent works: smaller, dimmed text inside a capped-height\n * (max-h-40) window always pinned to the newest line, with a Cursor-style\n * top dissolve once full (soft gradient fade at the top only — the text reads\n * as scrolling up and dissolving; the bottom stays sharp) — framed by the\n * breathing accent left-border glow. The window is intentionally NOT\n * user-scrollable (overflow-hidden, no scrollbar): the prose is ambient\n * feedback that scrolls up and dissolves; the full accumulated reasoning\n * stays readable afterwards via the message's reasoning details. The window\n * (and the status bubble above it) disappears the moment the final answer\n * starts flowing.\n */\nexport function ThinkingTextBubble({ content }: { content: string }) {\n const ref = useRef<HTMLDivElement>(null);\n const [isOverflowing, setIsOverflowing] = useState(false);\n const cleaned = cleanReasoningText(content);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n el.scrollTop = el.scrollHeight;\n setIsOverflowing(el.scrollHeight > el.clientHeight + 1);\n }, [cleaned]);\n\n if (cleaned.length < 3) return null;\n\n return (\n <div\n className=\"ml-11 mt-2.5 max-w-[75%] rounded-md border-l-2 bg-[var(--chat-accent)]/[0.03] py-2 pl-3 pr-3\"\n style={{ animation: 'reasoningGlow 3s ease-in-out infinite, chat-fade-in 0.5s ease-out' }}\n >\n <div\n ref={ref}\n className={`max-h-40 overflow-hidden${\n isOverflowing\n ? // -webkit- twin first: Safari/WebKit ignores unprefixed mask-image\n // on older versions, which would silently drop the top dissolve.\n ' [-webkit-mask-image:linear-gradient(to_bottom,transparent_0,rgb(0_0_0/0.25)_1.5rem,rgb(0_0_0/0.7)_3rem,black_4.5rem)]' +\n ' [mask-image:linear-gradient(to_bottom,transparent_0,rgb(0_0_0/0.25)_1.5rem,rgb(0_0_0/0.7)_3rem,black_4.5rem)]'\n : ''\n }`}\n >\n <p className=\"m-0 whitespace-pre-wrap break-words text-xs leading-5 text-gray-500 dark:text-white/45\">{cleaned}</p>\n </div>\n </div>\n );\n}\n\n/** Render seconds as a compact elapsed label (e.g. `45s`, `3m 20s`). */\nfunction formatElapsed(seconds: number): string {\n // Floor at the boundary: `elapsed_s` comes from the backend and may be a\n // float, which would otherwise render as \"45.3s\" / \"3m 20.5s\".\n const total = Math.floor(seconds);\n if (total < 60) return `${total}s`;\n const m = Math.floor(total / 60);\n const s = total % 60;\n return s > 0 ? `${m}m ${s}s` : `${m}m`;\n}\n\n/**\n * Elapsed time is only surfaced once the current operation has been\n * running long enough that the user could wonder whether it is stuck.\n */\nconst ELAPSED_DISPLAY_THRESHOLD_S = 15;\n\n/**\n * The reasoning window flips to the waiting game once nothing has progressed\n * for this long — i.e. no reasoning at all, or a reasoning stream that stalled.\n */\nconst STALL_DELAY_MS = 5000;\n\n/**\n * True once `signal` has stayed unchanged for `delayMs`. Re-arms whenever the\n * signal changes, so resumed reasoning clears the flag immediately. Used to\n * detect a stalled (or absent) reasoning stream so we can show the waiting game\n * in the meantime and flip back to the reasoning the moment it resumes. Arms no\n * timer (and never flips) while `enabled` is false, so a host that disables the\n * waiting game schedules no timeouts or re-renders for it.\n */\nfunction useStalled(signal: number, delayMs: number, enabled: boolean): boolean {\n const [stalled, setStalled] = useState(false);\n const prevSignalRef = useRef(signal);\n\n // Did the signal change since the last settled render? When it did, reasoning\n // has just resumed, so we report \"not stalled\" for this very render without a\n // render-phase state update (discouraged in React / brittle under StrictMode\n // and concurrent rendering). The effect below then resets the flag and re-arms\n // the timer — deriving the value here keeps the flip back to the reasoning\n // window free of the one-frame lag a clear-in-effect alone would leave.\n const signalChanged = prevSignalRef.current !== signal;\n\n useEffect(() => {\n prevSignalRef.current = signal;\n setStalled(false);\n if (!enabled) return;\n const id = window.setTimeout(() => setStalled(true), delayMs);\n return () => window.clearTimeout(id);\n }, [signal, delayMs, enabled]);\n\n return enabled && stalled && !signalChanged;\n}\n\nexport const ChatThinking = ({ agentStatus, logoIcon, t, miniGameEnabled = true }: ChatThinkingProps) => {\n const { label, StatusIcon, showDots } = resolveStatusVisual(agentStatus, t);\n const thinkingContent = agentStatus?.thinkingContent;\n const elapsedS = agentStatus?.elapsedS;\n const showElapsed = typeof elapsedS === 'number' && elapsedS >= ELAPSED_DISPLAY_THRESHOLD_S;\n // Show the waiting game when reasoning is absent or has stalled for 5s; flip\n // back to the reasoning window the moment new reasoning text resumes (the\n // accumulated content carries the continuation).\n const stalled = useStalled(thinkingContent?.length ?? 0, STALL_DELAY_MS, miniGameEnabled);\n const showGame = miniGameEnabled && stalled;\n\n return (\n <>\n <div className=\"flex gap-3 items-center justify-start\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[var(--chat-accent)]/15 to-[var(--chat-accent)]/5\">\n <span className=\"text-[var(--chat-accent)] [&>svg]:w-4 [&>svg]:h-4\">{logoIcon}</span>\n </div>\n <div className=\"rounded-lg bg-gray-50 dark:bg-white/[0.03] px-4 py-3 relative overflow-hidden\">\n <div className=\"absolute inset-0 bg-gradient-to-r from-[var(--chat-accent)]/[0.03] via-transparent to-[var(--chat-accent)]/[0.03] animate-pulse pointer-events-none\" />\n <div className=\"relative flex items-center gap-2.5\">\n {showDots ? (\n <div className=\"flex gap-[3px] items-center h-3.5 w-3.5 justify-center\">\n {[0, 0.15, 0.3].map((delay, i) => (\n <span\n key={i}\n className=\"h-[5px] w-[5px] rounded-full bg-[var(--chat-accent)]/50\"\n style={{ animation: `chat-dot 1s ease-in-out infinite ${delay}s` }}\n />\n ))}\n </div>\n ) : (\n <StatusIcon size={14} className=\"text-[var(--chat-accent)] animate-pulse transition-all duration-300\" />\n )}\n <span className=\"text-sm text-gray-500 dark:text-white/50 transition-all duration-300\">{label}</span>\n {showElapsed && <span className=\"text-xs text-gray-400 dark:text-white/30 tabular-nums shrink-0\">{formatElapsed(elapsedS)}</span>}\n </div>\n </div>\n </div>\n {thinkingContent && !showGame ? (\n <ThinkingTextBubble content={thinkingContent} />\n ) : showGame ? (\n <ChatWaitingGame t={t} enabled={miniGameEnabled} />\n ) : null}\n </>\n );\n};\n","import { useMemo, useState } from 'react';\nimport Markdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport { CheckIcon, CopyIcon } from './icons';\nimport { hardenNestedCodeFences, normalizeMarkdownTables } from '../utils';\n\ninterface MarkdownMessageProps {\n content: string;\n onRelativeLinkClick?: (href: string) => void;\n}\n\nconst isRelativeHref = (href?: string) => {\n if (!href) return false;\n if (href.startsWith('//')) return false;\n const hasAbsoluteScheme = /^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(href);\n return !hasAbsoluteScheme;\n};\n\n/**\n * Resolve an href to its host-app-internal form, or null when it points\n * elsewhere.\n *\n * Internal links must route through the host application's router\n * (`onRelativeLinkClick`) instead of a full page load / new tab. Two shapes\n * qualify:\n *\n * 1. Relative hrefs (`/dashboard/...`) — kept as-is.\n * 2. Absolute http(s) hrefs on the SAME origin as the embedding page\n * (e.g. `https://octi.example.com/dashboard/id/<uuid>`) — reduced to\n * `pathname + search + hash`. Backends intentionally emit absolute links\n * (so links work from any chat surface); when the chatbot is embedded in\n * that very platform the link must still navigate in-app.\n *\n * Anything else (other origins, non-http schemes, malformed URLs) returns\n * null and falls back to a regular new-tab anchor.\n */\nconst toInternalHref = (href?: string): string | null => {\n if (!href) return null;\n if (isRelativeHref(href)) return href;\n if (typeof window === 'undefined') return null;\n try {\n const url = new URL(href, window.location.href);\n if ((url.protocol === 'http:' || url.protocol === 'https:') && url.origin === window.location.origin) {\n return `${url.pathname}${url.search}${url.hash}` || '/';\n }\n } catch {\n /* malformed URL — treat as external */\n }\n return null;\n};\n\nexport const MarkdownMessage = ({ content, onRelativeLinkClick }: MarkdownMessageProps) => {\n const [copiedBlock, setCopiedBlock] = useState<string | null>(null);\n\n // Preprocess once per `content`: this component re-renders on UI-only state\n // (e.g. `copiedBlock`), and both passes scan the whole message, so memoizing\n // keeps that work off the hot path for large messages.\n const processedContent = useMemo(() => normalizeMarkdownTables(hardenNestedCodeFences(content)), [content]);\n\n const handleCopyCode = (code: string) => {\n navigator.clipboard.writeText(code);\n setCopiedBlock(code);\n setTimeout(() => setCopiedBlock(null), 2000);\n };\n\n return (\n <Markdown\n remarkPlugins={[remarkGfm]}\n components={{\n p: ({ children }) => <p className=\"mb-3 last:mb-0 leading-7 break-words text-[0.8125rem] text-gray-900 dark:text-white/90\">{children}</p>,\n code: ({ className, children }) => {\n const match = /language-(\\w+)/.exec(className || '');\n const codeStr = String(children).replace(/\\n$/, '');\n if (match) {\n return (\n <div className=\"my-3 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden bg-gray-50 dark:bg-white/[0.03]\">\n <div className=\"flex items-center justify-between px-3 py-1.5 border-b border-gray-200 dark:border-white/10 bg-gray-100 dark:bg-white/[0.03]\">\n <span className=\"text-[0.7rem] text-gray-500 dark:text-white/40 font-mono\">{match[1]}</span>\n <button\n type=\"button\"\n onClick={() => handleCopyCode(codeStr)}\n className=\"p-0.5 rounded-sm hover:bg-gray-200 dark:hover:bg-white/10 transition-colors\"\n >\n {copiedBlock === codeStr ? (\n <CheckIcon size={14} className=\"text-green-500\" />\n ) : (\n <CopyIcon size={14} className=\"text-gray-400 dark:text-white/40\" />\n )}\n </button>\n </div>\n <pre className=\"m-0 px-3 py-2 overflow-x-auto\">\n <code className=\"font-mono text-xs leading-[1.7] text-gray-800 dark:text-white/90 whitespace-pre\">{codeStr}</code>\n </pre>\n </div>\n );\n }\n return (\n <code className=\"bg-gray-100 dark:bg-white/[0.08] px-1.5 py-0.5 rounded-sm font-mono text-xs text-[var(--chat-accent)]\">{children}</code>\n );\n },\n ul: ({ children }) => (\n <ul className=\"pl-5 mb-3 text-[0.8125rem] text-gray-900 dark:text-white/90 [&_li]:mb-1 marker:text-[var(--chat-accent)]/50\">{children}</ul>\n ),\n ol: ({ children }) => (\n <ol className=\"pl-5 mb-3 text-[0.8125rem] text-gray-900 dark:text-white/90 [&_li]:mb-1 marker:text-[var(--chat-accent)]/50\">{children}</ol>\n ),\n blockquote: ({ children }) => (\n <blockquote className=\"my-3 border-l-2 border-[var(--chat-accent)]/30 bg-[var(--chat-accent)]/[0.03] pl-4 pr-3 py-2 rounded-r-md italic text-gray-500 dark:text-white/60\">\n {children}\n </blockquote>\n ),\n a: ({ href, children }) => {\n const internalHref = toInternalHref(href);\n const routeInternally = internalHref !== null && !!onRelativeLinkClick;\n const openInNewTab = !routeInternally && !isRelativeHref(href);\n const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {\n if (!routeInternally) return;\n event.preventDefault();\n onRelativeLinkClick!(internalHref!);\n };\n\n return (\n <a\n href={href}\n onClick={handleClick}\n target={openInNewTab ? '_blank' : undefined}\n rel={openInNewTab ? 'noopener noreferrer' : undefined}\n className=\"text-[var(--chat-accent)] underline underline-offset-2 hover:brightness-125\"\n >\n {children}\n </a>\n );\n },\n h1: ({ children }) => <h1 className=\"mt-4 first:mt-0 mb-2 font-bold text-base text-gray-900 dark:text-white\">{children}</h1>,\n h2: ({ children }) => <h2 className=\"mt-3 first:mt-0 mb-2 font-bold text-[0.9rem] text-gray-900 dark:text-white\">{children}</h2>,\n h3: ({ children }) => <h3 className=\"mt-3 first:mt-0 mb-1.5 font-semibold text-[0.85rem] text-gray-900 dark:text-white\">{children}</h3>,\n table: ({ children }) => (\n <div className=\"my-3 overflow-x-auto rounded-lg border border-gray-200 dark:border-white/10\">\n <table className=\"w-full border-collapse text-xs\">{children}</table>\n </div>\n ),\n th: ({ children }) => (\n <th className=\"px-3 py-2 text-left font-semibold bg-gray-50 dark:bg-white/[0.04] border-b border-gray-200 dark:border-white/10 text-gray-900 dark:text-white\">\n {children}\n </th>\n ),\n td: ({ children }) => (\n <td className=\"px-3 py-2 border-b border-gray-200 dark:border-white/10 text-gray-700 dark:text-white/80\">{children}</td>\n ),\n }}\n >\n {processedContent}\n </Markdown>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport type { ChatMessage, ToolCallTraceEntry } from '../types';\nimport {\n AlertTriangleIcon,\n ArrowRightLeftIcon,\n BotIcon,\n BrainIcon,\n CheckCircleIcon,\n ChevronDownIcon,\n CloseIcon,\n WrenchIcon,\n XCircleIcon,\n} from './icons';\nimport { cleanReasoningText } from './ChatThinking';\nimport { findChatbotRoot } from '../utils';\n\n/** Trace values longer than this are shown raw instead of pretty-printed JSON. */\nconst TRACE_PRETTY_LIMIT = 10_000;\n\n/**\n * Pretty-print a tool-call input/output when it is compact JSON; anything\n * else (plain text, oversized payloads, malformed JSON) is shown raw.\n */\nfunction prettyTraceValue(raw: string | undefined): string {\n if (!raw) return '';\n if (raw.length > TRACE_PRETTY_LIMIT) return raw;\n try {\n return JSON.stringify(JSON.parse(raw), null, 2);\n } catch {\n return raw;\n }\n}\n\nfunction toolDisplayName(name: string): string {\n return name.replace(/_/g, ' ');\n}\n\n/** Expandable row for a single tool call in the reasoning-details dialog. */\nconst ToolCallRow = ({ entry, index, t }: { entry: ToolCallTraceEntry; index: number; t: (key: string) => string }) => {\n const [expanded, setExpanded] = useState(false);\n\n const inputDisplay = useMemo(() => prettyTraceValue(entry.input), [entry.input]);\n const outputDisplay = useMemo(() => prettyTraceValue(entry.output), [entry.output]);\n const hasInput = !!inputDisplay && inputDisplay !== '{}';\n\n return (\n <div className=\"border border-gray-200 dark:border-white/[0.06] rounded-md overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-expanded={expanded}\n className=\"w-full flex items-center gap-2.5 px-3 py-2 hover:bg-gray-50 dark:hover:bg-white/[0.03] transition-colors text-left\"\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-[var(--chat-accent)]/10 text-[0.65rem] font-medium text-[var(--chat-accent)]\">\n {index + 1}\n </span>\n {entry.success ? (\n <CheckCircleIcon size={12} className=\"shrink-0 text-emerald-500 dark:text-emerald-400\" />\n ) : (\n <XCircleIcon size={12} className=\"shrink-0 text-red-500 dark:text-red-400\" />\n )}\n <span className=\"flex-1 min-w-0 text-[0.8125rem] text-gray-700 dark:text-white/80 truncate font-mono\">{toolDisplayName(entry.name)}</span>\n <ChevronDownIcon\n size={14}\n className={`shrink-0 text-gray-400 dark:text-white/50 transition-transform duration-200 ${expanded ? 'rotate-180' : ''}`}\n />\n </button>\n\n {expanded && (\n <div className=\"border-t border-gray-200 dark:border-white/[0.06]\">\n {hasInput && (\n <div className=\"px-3 py-2 border-b border-gray-100 dark:border-white/[0.04] bg-gray-50/50 dark:bg-white/[0.01]\">\n <p className=\"m-0 mb-1.5 text-[0.6rem] text-gray-500 dark:text-white/40 uppercase tracking-wider font-medium\">{t('Input')}</p>\n <pre className=\"m-0 text-[0.7rem] text-gray-600 dark:text-white/60 font-mono whitespace-pre-wrap break-all leading-relaxed max-h-40 overflow-y-auto filigran-chat-scrollable\">\n {inputDisplay}\n </pre>\n </div>\n )}\n <div className=\"px-3 py-2 bg-gray-50/50 dark:bg-white/[0.01]\">\n <p className=\"m-0 mb-1.5 text-[0.6rem] text-gray-500 dark:text-white/40 uppercase tracking-wider font-medium\">{t('Output')}</p>\n <pre className=\"m-0 text-[0.7rem] text-gray-600 dark:text-white/60 font-mono whitespace-pre-wrap break-all leading-relaxed max-h-48 overflow-y-auto filigran-chat-scrollable\">\n {outputDisplay || t('(no output)')}\n </pre>\n </div>\n </div>\n )}\n </div>\n );\n};\n\ninterface ReasoningDetailsDialogProps {\n msg: ChatMessage;\n onClose: () => void;\n t: (key: string) => string;\n}\n\n/**\n * Modal dialog with the full reasoning details of an assistant message —\n * mirrors the XTM One web chat dialog (truncation warning, model reasoning,\n * expandable per-tool-call trace, transfer chain). Rendered as an overlay\n * covering the chatbot panel so it works in every mode and host without\n * depending on the host app's stacking order.\n */\nexport const ReasoningDetailsDialog = ({ msg, onClose, t }: ReasoningDetailsDialogProps) => {\n const hostRef = useRef<HTMLSpanElement>(null);\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n const dialogRef = useRef<HTMLDivElement>(null);\n const [root, setRoot] = useState<HTMLElement | null>(null);\n\n useEffect(() => {\n setRoot(findChatbotRoot(hostRef.current));\n }, []);\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n onClose();\n return;\n }\n // Focus trap: aria-modal promises focus stays inside the dialog, so\n // Tab/Shift+Tab cycle within it instead of escaping to the panel.\n if (e.key !== 'Tab') return;\n const dialog = dialogRef.current;\n if (!dialog) return;\n const focusable = dialog.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');\n if (focusable.length === 0) return;\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n if (e.shiftKey) {\n if (active === first || !dialog.contains(active)) {\n e.preventDefault();\n last.focus();\n }\n } else if (active === last || !dialog.contains(active)) {\n e.preventDefault();\n first.focus();\n }\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [onClose]);\n\n // Move initial keyboard focus into the modal (aria-modal) once the portal\n // is mounted, and hand it back to the trigger when the dialog closes.\n useEffect(() => {\n if (!root) return;\n const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n closeButtonRef.current?.focus({ preventScroll: true });\n return () => previouslyFocused?.focus({ preventScroll: true });\n }, [root]);\n\n // Prefer the backend's explicit count, then the detailed trace (what the\n // dialog body actually renders), then the flat tool-name list — keeps the\n // header summary consistent with the rows below.\n const totalCalls = msg.toolCallCount ?? msg.toolCallTrace?.length ?? msg.toolNames?.length ?? 0;\n const tools = msg.toolNames ?? [];\n const iterations = msg.iterations ?? 1;\n const transfers = msg.transferChain ?? [];\n const trace = msg.toolCallTrace ?? [];\n const reasoning = (msg.reasoning ?? '').trim();\n\n const summaryParts = [\n iterations > 1 ? `${iterations} ${t('iterations')}` : '',\n `${totalCalls} ${totalCalls === 1 ? t('tool call') : t('tool calls')}`,\n transfers.length > 0 ? `${transfers.length} ${transfers.length === 1 ? t('transfer') : t('transfers')}` : '',\n ].filter(Boolean);\n\n return (\n <span ref={hostRef} className=\"hidden\">\n {root &&\n createPortal(\n <div\n className=\"absolute inset-0 z-[10000] flex items-center justify-center bg-black/30 dark:bg-black/50 p-4\"\n onClick={onClose}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('Reasoning details')}\n onClick={(e) => e.stopPropagation()}\n className=\"w-full max-w-md max-h-full flex flex-col rounded-xl border border-gray-200 dark:border-white/10 bg-white dark:bg-[#1e1e2e] shadow-[0_8px_32px_rgba(0,0,0,0.25)] dark:shadow-[0_8px_32px_rgba(0,0,0,0.6)]\"\n >\n <div className=\"px-4 pt-3.5 pb-2.5 border-b border-gray-200 dark:border-white/10\">\n <div className=\"flex items-center gap-2\">\n <WrenchIcon size={15} className=\"text-[var(--chat-accent)]\" />\n <span className=\"flex-1 text-[0.875rem] font-semibold text-gray-900 dark:text-white\">{t('Reasoning details')}</span>\n <button\n ref={closeButtonRef}\n type=\"button\"\n onClick={onClose}\n aria-label={t('Close')}\n className=\"w-7 h-7 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <CloseIcon size={16} />\n </button>\n </div>\n <p className=\"m-0 mt-0.5 text-[0.72rem] text-gray-500 dark:text-white/40\">{summaryParts.join(' · ')}</p>\n </div>\n\n <div className=\"px-4 py-3 overflow-y-auto filigran-chat-scrollable flex flex-col gap-3\">\n {msg.isTruncated && (\n <div className=\"flex items-start gap-2.5 rounded-md border border-amber-500/20 bg-amber-500/5 px-3 py-2.5 text-[0.72rem] text-amber-600 dark:text-amber-300/90\">\n <AlertTriangleIcon size={14} className=\"shrink-0 mt-0.5 text-amber-500 dark:text-amber-400\" />\n <span>\n <span className=\"font-semibold\">{t('Turn limit reached.')}</span>{' '}\n {t(\n \"The agent's iteration budget was exhausted - execution stopped before completing all planned steps. The final response is a best-effort summary of work done so far.\",\n )}\n </span>\n </div>\n )}\n\n {reasoning && (\n <div>\n <div className=\"flex items-center gap-1.5 mb-1.5\">\n <BrainIcon size={13} className=\"text-[var(--chat-accent)]/70\" />\n <span className=\"text-[0.72rem] font-medium text-gray-500 dark:text-white/50\">{t('Model reasoning')}</span>\n </div>\n <div className=\"rounded-md border border-gray-200 dark:border-white/[0.06] bg-gray-50/50 dark:bg-white/[0.01] px-2.5 py-2 max-h-44 overflow-y-auto filigran-chat-scrollable\">\n <p className=\"m-0 whitespace-pre-wrap break-words text-[0.72rem] leading-5 text-gray-500 dark:text-white/45\">\n {cleanReasoningText(reasoning)}\n </p>\n </div>\n </div>\n )}\n\n {trace.length > 0 ? (\n <div className=\"flex flex-col gap-1.5\">\n {trace.map((entry, i) => (\n <ToolCallRow key={`${entry.name}-${i}`} entry={entry} index={i} t={t} />\n ))}\n </div>\n ) : (\n tools.length > 0 && (\n // Fallback when no detailed trace is available (legacy\n // messages / backends without trace support).\n <div>\n {tools.map((tn, i) => (\n <div\n key={`${tn}-${i}`}\n className=\"flex items-center gap-3 py-2 border-b border-gray-100 dark:border-white/[0.04] last:border-0\"\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-[var(--chat-accent)]/10 text-[0.65rem] font-medium text-[var(--chat-accent)]\">\n {i + 1}\n </span>\n <WrenchIcon size={12} className=\"shrink-0 text-gray-400 dark:text-white/50\" />\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/80 truncate font-mono\">{toolDisplayName(tn)}</span>\n </div>\n ))}\n </div>\n )\n )}\n\n {transfers.length > 0 && (\n <div>\n <div className=\"flex items-center gap-1.5 mb-1.5\">\n <ArrowRightLeftIcon size={13} className=\"text-[var(--chat-accent)]/70\" />\n <span className=\"text-[0.72rem] font-medium text-gray-500 dark:text-white/50\">{t('Transfer chain')}</span>\n </div>\n <div className=\"flex items-center gap-1.5 flex-wrap\">\n {transfers.map((tr, i) => (\n // Composite key: the same agent can appear twice in a\n // chain (A -> B -> A) and older payloads have no id.\n <div key={`${tr.agentId}-${i}`} className=\"flex items-center gap-1.5\">\n {i > 0 && <span className=\"text-gray-300 dark:text-white/30 text-[0.72rem]\">→</span>}\n <span className=\"inline-flex items-center gap-1 rounded-md bg-[var(--chat-accent)]/10 px-2 py-0.5 text-[0.72rem] font-medium text-[var(--chat-accent)]\">\n <BotIcon size={12} />\n {tr.agentName}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n </div>\n </div>,\n root,\n )}\n </span>\n );\n};\n","import { useEffect, useRef, useState } from 'react';\nimport type { AgentStatusState, ChatAttachment, ChatMessage } from '../types';\nimport { splitFileMarkers } from '../utils';\nimport { AlertTriangleIcon, DownloadIcon, FileIcon, InfoIcon } from './icons';\nimport { ChatThinking } from './ChatThinking';\nimport { MarkdownMessage } from './MarkdownMessage';\nimport { ReasoningDetailsDialog } from './ReasoningDetailsDialog';\n\ninterface ChatMessagesProps {\n messages: ChatMessage[];\n isLoading: boolean;\n agentStatus: AgentStatusState | null;\n agentName: string;\n logoIcon: React.ReactNode;\n onRelativeLinkClick?: (href: string) => void;\n /** Download an agent-generated file via the host app's backend proxy. */\n onDownloadFile?: (attachment: ChatAttachment) => void;\n /** Host-level override for the waiting mini-game / dynamic messages. */\n miniGameEnabled?: boolean;\n t: (key: string) => string;\n}\n\nfunction formatFileSize(bytes?: number): string {\n if (!bytes || bytes <= 0) return '';\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/** Short uppercase extension label for a file chip (e.g. `report.pdf` → `PDF`). */\nfunction fileExtensionLabel(filename: string): string | undefined {\n const dot = filename.lastIndexOf('.');\n if (dot <= 0 || dot === filename.length - 1) return undefined;\n const ext = filename.slice(dot + 1);\n return ext.length <= 8 ? ext.toUpperCase() : undefined;\n}\n\nexport const ChatMessages = ({\n messages,\n isLoading,\n agentStatus,\n agentName,\n logoIcon,\n onRelativeLinkClick,\n onDownloadFile,\n miniGameEnabled = true,\n t,\n}: ChatMessagesProps) => {\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const [toolDetailMsgId, setToolDetailMsgId] = useState<string | null>(null);\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, [messages]);\n\n // Keep the bottom in view while the reasoning window below the status\n // bubble grows: thinking prose streams in without any `messages` change,\n // so without this the growing window slides under the fold and the user\n // stops seeing the live reasoning. `behavior: 'instant'` (CSSOM View,\n // Baseline-supported) forces a non-animated jump — this fires on every\n // reasoning chunk and smooth animations would queue up; 'auto' would not\n // do, since a `scroll-behavior: smooth` ancestor turns it smooth again.\n const thinkingLen = agentStatus?.thinkingContent?.length ?? 0;\n useEffect(() => {\n if (!thinkingLen) return;\n messagesEndRef.current?.scrollIntoView({ behavior: 'instant' });\n }, [thinkingLen]);\n\n const renderAttachmentCard = (att: ChatAttachment, key: string) => {\n const isWorking = att.fileTag === 'working_file';\n const sizeLabel = formatFileSize(att.size);\n return (\n <button\n key={key}\n type=\"button\"\n onClick={() => onDownloadFile?.(att)}\n title={t('Download')}\n className={`group flex items-center gap-2 text-left rounded-lg border px-2.5 py-1.5 transition-colors cursor-pointer max-w-[90%] ${\n isWorking\n ? 'border-gray-200 dark:border-white/10 bg-transparent'\n : 'border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/[0.04] hover:border-[var(--chat-accent)] hover:bg-[var(--chat-accent)]/5'\n }`}\n >\n <span className={`shrink-0 ${isWorking ? 'text-gray-400 dark:text-white/40' : 'text-[var(--chat-accent)]'}`}>\n <FileIcon size={16} />\n </span>\n <span className=\"flex flex-col min-w-0 flex-1\">\n <span className=\"truncate text-[0.75rem] text-gray-900 dark:text-white\">{att.filename}</span>\n {(att.type || sizeLabel) && (\n <span className=\"text-[0.65rem] text-gray-400 dark:text-white/40 uppercase\">{[att.type, sizeLabel].filter(Boolean).join(' · ')}</span>\n )}\n </span>\n <span className=\"shrink-0 text-gray-400 dark:text-white/30 group-hover:text-[var(--chat-accent)]\">\n <DownloadIcon size={15} />\n </span>\n </button>\n );\n };\n\n // Render assistant content as an ordered interleave of prose segments and\n // download cards, so a reply with markers like\n // `text [[FILE:a]] more text [[FILE:b]]` keeps the cards at their source\n // position. Cards only render when a download handler is wired\n // (`onDownloadFile`); attachments whose marker isn't found in the prose are\n // appended as a fallback. During streaming the attachments aren't hydrated\n // yet, so only prose (markers stripped) renders.\n const buildAssistantBlocks = (msg: ChatMessage, loading: boolean): React.ReactNode[] => {\n const parts = splitFileMarkers(msg.content);\n const attByFileId = new Map((msg.attachments ?? []).map((a) => [a.fileId, a] as const));\n const used = new Set<string>();\n const blocks: React.ReactNode[] = [];\n\n parts.forEach((part, i) => {\n if (part.type === 'text') {\n if (part.value.trim()) {\n blocks.push(\n <div key={`t-${i}`} className=\"max-w-[90%] pl-1 py-1 text-[0.8125rem] leading-7\">\n <MarkdownMessage content={part.value} onRelativeLinkClick={onRelativeLinkClick} />\n </div>,\n );\n }\n } else if (onDownloadFile) {\n const att = attByFileId.get(part.fileId);\n if (att) {\n used.add(part.fileId);\n blocks.push(renderAttachmentCard(att, `f-${part.fileId}-${i}`));\n }\n }\n });\n\n if (onDownloadFile) {\n (msg.attachments ?? []).forEach((att) => {\n if (!used.has(att.fileId)) {\n blocks.push(renderAttachmentCard(att, `orphan-${att.fileId}`));\n }\n });\n }\n\n // An assistant reply that is *only* a file marker leaves no prose; show a\n // subtle ellipsis (not an empty padded bubble) when nothing else rendered\n // and we're not still streaming.\n if (blocks.length === 0 && !loading) {\n blocks.push(\n <span key=\"empty\" className=\"pl-1 text-[0.8125rem] text-gray-400 dark:text-white/40 italic\">\n ...\n </span>,\n );\n }\n\n return blocks;\n };\n\n // A user-uploaded file shown as a non-clickable chip — used while the upload\n // is still in flight (no server `fileId` yet) or when no download handler is\n // wired by the host.\n const renderFileChip = (name: string, key: string) => (\n <span\n key={key}\n className=\"inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-gray-200 dark:border-white/10 text-[0.7rem] text-gray-600 dark:text-white/60\"\n >\n <FileIcon size={14} />\n {name}\n </span>\n );\n\n // Build the file cards shown on a user message. A successfully-uploaded file\n // carries a server `fileId`, so it renders as the same download card as an\n // agent-generated attachment (re-using the host download proxy via\n // `onDownloadFile`) — uploaded files must stay downloadable, not just\n // displayed. Files still uploading (no `fileId` / not `done`) or hosts\n // without a download handler fall back to a static chip. On conversation\n // restore the backend re-surfaces user uploads as `attachments` (there are\n // no live `files`), so those are rendered too.\n const buildUserFileBlocks = (msg: ChatMessage): React.ReactNode[] => {\n const blocks: React.ReactNode[] = [];\n const seen = new Set<string>();\n\n (msg.files ?? []).forEach((f, i) => {\n const downloadable = !!(onDownloadFile && f.fileId && f.uploadStatus === 'done');\n if (downloadable && f.fileId) {\n seen.add(f.fileId);\n blocks.push(\n renderAttachmentCard(\n { fileId: f.fileId, filename: f.name, type: fileExtensionLabel(f.name), size: f.size, contentType: f.type },\n `file-${f.fileId}-${i}`,\n ),\n );\n } else {\n blocks.push(renderFileChip(f.name, `file-${i}`));\n }\n });\n\n (msg.attachments ?? []).forEach((att, i) => {\n if (seen.has(att.fileId)) return;\n seen.add(att.fileId);\n blocks.push(onDownloadFile ? renderAttachmentCard(att, `att-${att.fileId}-${i}`) : renderFileChip(att.filename, `att-${i}`));\n });\n\n return blocks;\n };\n\n // The streaming response is the LAST ASSISTANT message — not necessarily\n // the last message overall: a mid-run steering send appends an optimistic\n // user bubble after the assistant message that is still streaming. Gating\n // on `messages.length - 1` would then drop the live cursor / ChatThinking\n // state the moment the user steers.\n let lastAssistantIndex = -1;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === 'assistant') {\n lastAssistantIndex = i;\n break;\n }\n }\n\n return (\n <div className=\"flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-4 filigran-chat-scrollable\">\n {messages.map((msg, index) => {\n const isAssistant = msg.role === 'assistant';\n const isEmpty = !msg.content;\n // The live cursor / thinking bubble / ChatThinking state — and,\n // conversely, the hiding of the completed-message affordances (the\n // reasoning \"i\" button) — must be gated on the streaming message.\n // Gating those on the global `isLoading` instead made the blinking\n // cursor leak onto every prior assistant message and the \"i\" button\n // vanish from all of them while a *later* response was streaming.\n const isStreamingMessage = isLoading && index === lastAssistantIndex;\n const isThinking = isAssistant && isEmpty && isStreamingMessage;\n\n if (isThinking) {\n return (\n <div key={msg.id}>\n <ChatThinking agentStatus={agentStatus} logoIcon={logoIcon} t={t} miniGameEnabled={miniGameEnabled} />\n </div>\n );\n }\n\n return (\n <div key={msg.id} className={`flex flex-col ${isAssistant ? 'items-start' : 'items-end'}`}>\n {isAssistant && (\n <div className=\"flex items-center gap-1.5 mb-1\">\n <div className=\"w-6 h-6 rounded-lg flex items-center justify-center bg-gradient-to-br from-[var(--chat-accent)]/20 to-[var(--chat-accent)]/5\">\n <span className=\"text-[var(--chat-accent)] [&>svg]:w-3 [&>svg]:h-3\">{logoIcon}</span>\n </div>\n <span className=\"font-semibold text-xs text-gray-900 dark:text-white\">{agentName}</span>\n </div>\n )}\n\n {!isAssistant && ((msg.files?.length ?? 0) > 0 || (msg.attachments?.length ?? 0) > 0) && (\n <div className=\"flex gap-1.5 flex-wrap mb-1.5 justify-end\">{buildUserFileBlocks(msg)}</div>\n )}\n\n {isAssistant ? (\n <div className=\"flex flex-col gap-1.5 w-full items-start\">\n {buildAssistantBlocks(msg, isStreamingMessage)}\n {!isEmpty && isStreamingMessage && (\n <span className=\"inline-block w-1.5 h-4 bg-[var(--chat-accent)]/70 rounded-xs ml-1 animate-pulse\" />\n )}\n </div>\n ) : (\n <div className=\"max-w-[90%] px-3.5 py-2 rounded-[14px_14px_4px_14px] bg-[var(--chat-accent-dark)] text-white text-[0.8125rem] leading-6\">\n {msg.content}\n </div>\n )}\n\n {isAssistant &&\n !isEmpty &&\n !isStreamingMessage &&\n ((msg.toolNames && msg.toolNames.length > 0) ||\n (msg.reasoning ?? '').trim() ||\n (msg.toolCallTrace && msg.toolCallTrace.length > 0) ||\n (msg.transferChain && msg.transferChain.length > 0) ||\n msg.isTruncated) && (\n <>\n <button\n type=\"button\"\n onClick={() => setToolDetailMsgId(toolDetailMsgId === msg.id ? null : msg.id)}\n className={`mt-0.5 p-1 rounded-lg transition-opacity ${\n msg.isTruncated\n ? // A truncated turn must be visible at a glance (not\n // gated on hover) so the user notices the warning —\n // mirrors the XTM One web chat affordance.\n 'opacity-100 text-amber-500 dark:text-amber-400 hover:text-amber-600 dark:hover:text-amber-300'\n : 'opacity-50 hover:opacity-100 hover:text-[var(--chat-accent)]'\n }`}\n title={msg.isTruncated ? t('Reasoning details — turn limit reached') : t('Reasoning details')}\n aria-label={msg.isTruncated ? t('Reasoning details — turn limit reached') : t('Reasoning details')}\n aria-haspopup=\"dialog\"\n aria-expanded={toolDetailMsgId === msg.id}\n >\n {msg.isTruncated ? <AlertTriangleIcon size={14} /> : <InfoIcon size={14} />}\n </button>\n {toolDetailMsgId === msg.id && <ReasoningDetailsDialog msg={msg} onClose={() => setToolDetailMsgId(null)} t={t} />}\n </>\n )}\n </div>\n );\n })}\n <div ref={messagesEndRef} />\n </div>\n );\n};\n","interface ChatWelcomeProps {\n firstName: string;\n logoIcon: React.ReactNode;\n promptSuggestions: string[];\n onPromptClick: (prompt: string) => void;\n t: (key: string) => string;\n}\n\nexport const ChatWelcome = ({ firstName, logoIcon, promptSuggestions, onPromptClick, t }: ChatWelcomeProps) => (\n <div className=\"flex-1 flex flex-col items-center justify-center px-6 pb-8\">\n <span className=\"text-[var(--chat-accent)] mb-4 [&>svg]:w-12 [&>svg]:h-12 drop-shadow-[0_0_12px_var(--chat-accent-40)]\">{logoIcon}</span>\n <h2 className=\"text-xl font-medium mb-6 text-center text-gray-900 dark:text-white\" style={{ fontFamily: '\"Geologica\", sans-serif' }}>\n {t('How can I help you, ')}\n {firstName}?\n </h2>\n <div className=\"w-full max-w-[320px]\">\n <span className=\"block text-center mb-2 text-[0.65rem] tracking-[1.5px] uppercase text-[var(--chat-accent)] font-semibold\">\n {t('Suggestions')}\n </span>\n {promptSuggestions.map((prompt) => (\n <button\n key={prompt}\n type=\"button\"\n onClick={() => onPromptClick(prompt)}\n className=\"w-full text-left text-[0.8125rem] text-gray-800 dark:text-white py-1.5 px-3 mb-1 rounded-lg border border-gray-200 dark:border-white/10 bg-transparent transition-colors hover:bg-[var(--chat-accent-10)] hover:border-[var(--chat-accent-50)]\"\n >\n {t(prompt)}\n </button>\n ))}\n </div>\n </div>\n);\n","import { type FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';\nimport type { ChatAttachment, ChatMessage, ChatPanelProps } from '../types';\nimport { hexAlpha, identity } from '../utils';\nimport { parseAttachments, parseToolCallTrace, parseTransferChain } from '../hooks/protocols/parseRestEvent';\nimport { useChat } from '../hooks/useChat';\nimport { useAgents } from '../hooks/useAgents';\nimport { useConversations } from '../hooks/useConversations';\nimport { useSidebarResize } from '../hooks/useSidebarResize';\nimport { useAwayCompletionNotice } from '../hooks/useAwayCompletionNotice';\nimport { DefaultLogoIcon } from './icons';\nimport { ChatHeader } from './ChatHeader';\nimport { ChatInput } from './ChatInput';\nimport { ChatMessages } from './ChatMessages';\nimport { ChatWelcome } from './ChatWelcome';\n\nconst FLOATING_WIDTH = 380;\nconst FLOATING_HEIGHT = 560;\nconst SIDEBAR_GAP = 6;\n\nconst DEFAULT_SUGGESTIONS = [\n 'Help me create a new simulation scenario',\n 'What are the latest attack patterns?',\n 'How do I configure detection rules?',\n 'Summarize my recent findings',\n];\n\nexport const ChatPanel: FunctionComponent<ChatPanelProps> = ({\n mode,\n onClose,\n onModeChange,\n topOffset = 0,\n apiBaseUrl,\n apiEndpoints,\n agentDashboardUrl,\n user,\n t = identity,\n accentColor = '#7b5cff',\n logoIcon,\n promptSuggestions = DEFAULT_SUGGESTIONS,\n draftBorderColor,\n resizable = false,\n onWidthChange,\n onResizeStart,\n onResizeEnd,\n disableFileManagement = false,\n onRelativeLinkClick,\n onDownloadError,\n maxFileCount,\n maxTotalSize,\n requestHeaders,\n pageContext,\n pushContentSelector,\n backendType = 'rest',\n miniGameEnabled = true,\n notifyOnComplete = true,\n onTaskComplete,\n}) => {\n const [modeMenuOpen, setModeMenuOpen] = useState(false);\n\n const { agents, selectedAgent, agentMenuOpen, setAgentMenuOpen, handleSwitchAgent } = useAgents({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n });\n\n const {\n messages,\n inputValue,\n setInputValue,\n isLoading,\n agentStatus,\n attachedFiles,\n conversationId,\n transferredAgent,\n canSteer,\n historyLoadedRef,\n conversationIdRef,\n handleFileAdd,\n handlePaste,\n handleSendMessage,\n handleNewChat,\n handleStopGenerating,\n setAttachedFiles,\n setMessages,\n updateConversationId,\n handleSwitchConversation,\n } = useChat({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n agentSlug: selectedAgent?.slug,\n requestHeaders,\n pageContext,\n t,\n maxFileCount,\n maxTotalSize,\n });\n\n const { historyEnabled, conversations, conversationsLoading, refreshConversations, deleteConversation } = useConversations({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n });\n const [historyMenuOpen, setHistoryMenuOpen] = useState(false);\n\n const handleHistoryMenuToggle = () => {\n // Computed from the committed state in the event handler — NOT inside the\n // state updater, which must stay pure (StrictMode/concurrent rendering may\n // invoke updaters more than once, which would duplicate the fetch).\n const next = !historyMenuOpen;\n if (next) {\n // Fetch lazily on open so the list reflects the latest server state\n // (titles are rewritten by the backend after the first message).\n void refreshConversations();\n }\n setHistoryMenuOpen(next);\n };\n\n const handleSelectConversation = (id: string) => {\n setHistoryMenuOpen(false);\n handleSwitchConversation(id);\n };\n\n const handleDeleteConversation = async (id: string) => {\n const deleted = await deleteConversation(id);\n // Deleting the active conversation resets to a fresh chat so the next\n // message doesn't target a dead conversation id.\n if (deleted && id === conversationIdRef.current) {\n handleNewChat();\n }\n };\n\n const { sidebarWidth, handleResizeStart, defaultWidth, isResizing } = useSidebarResize({\n mode,\n resizable,\n onWidthChange,\n onResizeStart,\n onResizeEnd,\n });\n\n // Push content when sidebar mode is active using CSS variable\n useEffect(() => {\n const width = mode === 'sidebar' ? (resizable ? sidebarWidth : defaultWidth) : 0;\n const pushWidth = width > 0 ? width + SIDEBAR_GAP : 0;\n\n // Set CSS variable on :root for any component to use\n document.documentElement.style.setProperty('--chatbot-sidebar-width', `${pushWidth}px`);\n document.documentElement.style.setProperty('--chatbot-transition', isResizing ? 'none' : 'all 225ms cubic-bezier(0.4, 0, 0.2, 1)');\n\n // Also apply to pushContentSelector if provided (for simple cases)\n if (pushContentSelector) {\n const contentElement = document.querySelector<HTMLElement>(pushContentSelector);\n if (contentElement) {\n const originalPaddingRight = contentElement.style.paddingRight;\n const originalTransition = contentElement.style.transition;\n\n contentElement.style.paddingRight = pushWidth > 0 ? `${pushWidth}px` : '';\n contentElement.style.transition = isResizing ? 'none' : 'padding-right 225ms cubic-bezier(0.4, 0, 0.2, 1)';\n\n return () => {\n contentElement.style.paddingRight = originalPaddingRight;\n contentElement.style.transition = originalTransition;\n document.documentElement.style.setProperty('--chatbot-sidebar-width', '0px');\n };\n }\n }\n\n return () => {\n document.documentElement.style.setProperty('--chatbot-sidebar-width', '0px');\n };\n }, [pushContentSelector, mode, sidebarWidth, defaultWidth, resizable, isResizing]);\n\n const resolvedLogo = logoIcon ?? <DefaultLogoIcon size={24} />;\n const firstName = user.firstName;\n const agentName = transferredAgent?.name || selectedAgent?.name || 'Assistant';\n\n // \"Viewing the chat\" must mean the panel is on screen in the active tab —\n // NOT that an element inside it currently holds focus. In sidebar (and\n // floating) mode the user routinely reads a streamed answer while their focus\n // stays in the host app, so the previous focus-within test\n // (`document.activeElement.closest('.filigran-chatbot.fixed')`) wrongly\n // classified them as \"not viewing\" and fired a redundant completion toast for\n // an answer sitting right in front of them. The host mounts `<ChatPanel/>`\n // only while the widget is open, so a mounted + visible panel root means the\n // answer is visible to the user; the notifier still treats a hidden tab or an\n // unfocused window as \"away\" and notifies there. A host that keeps the panel\n // mounted but `display:none` while \"closed\" is likewise reported as not\n // viewing (checkVisibility() === false), so completion still notifies. The\n // `.fixed` qualifier targets the panel root, never the toggle button (which\n // carries `.filigran-chatbot` alone). Stable reference (useCallback) so the\n // notifier's effect doesn't re-run on every render — the panel re-renders\n // frequently while a response streams; the DOM is queried live on each call.\n const isViewingChat = useCallback(() => {\n if (typeof document === 'undefined') return false;\n const panel = document.querySelector('.filigran-chatbot.fixed') as (HTMLElement & { checkVisibility?: () => boolean }) | null;\n if (!panel) return false;\n if (typeof panel.checkVisibility === 'function') return panel.checkVisibility();\n // Fallback for browsers without Element.checkVisibility(): a display:none\n // panel generates no layout box, so an empty client-rect list means hidden\n // (works for the position:fixed root, whose offsetParent is null even when\n // shown). Erring toward \"not viewing\" keeps the documented closed/hidden\n // path notifying instead of silently swallowing the notice on older engines.\n return panel.getClientRects().length > 0;\n }, []);\n\n // Notify when a long turn finishes and the user is not watching the chat —\n // away (tab hidden / another window / panel closed-or-hidden). An open,\n // on-screen panel in the focused tab counts as watching, so no toast fires\n // for an answer the user can already see.\n useAwayCompletionNotice({\n isLoading,\n agentName,\n t,\n enabled: notifyOnComplete,\n onComplete: onTaskComplete,\n isViewingChat,\n });\n\n // Download an agent-generated file. The URL is resolved against the host\n // app's own backend proxy (apiBaseUrl), NOT the upstream chat service:\n // the proxy mints any upstream token server-side, so the user stays\n // authenticated to the host platform (e.g. OpenCTI / OpenAEV) only and\n // never logs in to the upstream service. Same-origin cookies +\n // requestHeaders (CSRF / draft context) carry the host-app auth.\n // Downloads need a path to build the URL from. Enabled for the REST\n // backend unless explicitly disabled (`download === null`). In\n // single-endpoint mode there is no per-path routing, so a download path\n // must be provided explicitly (e.g. an OpenCTI-style proxy route);\n // otherwise the default REST `/chat/files` path is used.\n const downloadPathProvided = apiEndpoints?.download !== null && apiEndpoints?.download !== undefined;\n const canDownload = backendType === 'rest' && apiEndpoints?.download !== null && (!apiEndpoints?.singleEndpoint || downloadPathProvided);\n\n const handleDownloadFile = useCallback(\n async (att: ChatAttachment) => {\n const base = apiEndpoints?.download ?? '/chat/files';\n const url = `${apiBaseUrl}${base}/${encodeURIComponent(att.fileId)}/download`;\n try {\n const res = await fetch(url, {\n method: 'GET',\n credentials: 'include',\n headers: { ...(requestHeaders ?? {}) },\n });\n if (!res.ok) throw new Error(`Download failed: ${res.status}`);\n const blob = await res.blob();\n const objectUrl = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = objectUrl;\n link.download = att.filename || 'download';\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(objectUrl);\n } catch (err) {\n // Surface the failure (403/404/5xx/network) to the host so it can\n // notify the user — the chatbot has no toast surface of its own.\n // If the host doesn't provide a handler the error is intentionally\n // not thrown further (a rejected click handler has nowhere to go).\n onDownloadError?.(err, att);\n }\n },\n [apiBaseUrl, apiEndpoints, requestHeaders, onDownloadError],\n );\n\n const cssVars = {\n '--chat-accent': accentColor,\n '--chat-accent-10': hexAlpha(accentColor, 0.1),\n '--chat-accent-40': hexAlpha(accentColor, 0.25),\n '--chat-accent-50': hexAlpha(accentColor, 0.5),\n '--chat-accent-dark': accentColor,\n } as React.CSSProperties;\n\n // Tracks whether this panel is still mounted. Flipped to false ONLY on a\n // real unmount (empty-deps cleanup) — never on the benign teardowns that an\n // inline `apiEndpoints` / `requestHeaders` prop churn or a StrictMode\n // double-invoke trigger. The restore effect below reads it so an in-flight\n // `/chat/sessions` response that outlives the panel — the host renders\n // `<ChatPanel />` conditionally and the user closes it mid-request — can't\n // call `setMessages` / `updateConversationId` after unmount, while a restore\n // merely interrupted by a re-render still lands. Re-set to true on setup so\n // StrictMode's mount → unmount → remount of the same instance leaves it true.\n const isMountedRef = useRef(true);\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n // Load conversation history when agent is selected\n useEffect(() => {\n // Skip session history if disabled, using single endpoint mode, or non-REST backend\n if (apiEndpoints?.sessions === null || apiEndpoints?.singleEndpoint || backendType === 'legacy' || backendType === 'ag-ui') return;\n if (!conversationId || historyLoadedRef.current || !selectedAgent) return;\n historyLoadedRef.current = true;\n const sessionsUrl = `${apiBaseUrl}${apiEndpoints?.sessions ?? '/chat/sessions'}`;\n\n // The conversation this restore is being issued for. A host re-render that\n // churns an inline `apiEndpoints` / `requestHeaders` prop, or a React\n // StrictMode double-invoke, tears this effect down and re-runs it while the\n // request is still in flight — but the conversation itself hasn't changed.\n // We must NOT drop the restore in those benign cases (doing so left the\n // panel looking like a brand-new chat, randomly, on reload). Only a real\n // change — the user starting a new chat or switching agent, both of which\n // reset the id via `handleNewChat()` — should abandon the response, so it\n // can't resurrect a dead id or overwrite the freshly-started conversation.\n // Compare the live ref at apply time (not a blanket teardown flag) so the\n // legitimate restore always lands while a superseded one is still ignored —\n // and bail out entirely once the panel has actually unmounted, so a late\n // response can't write to localStorage / state after the panel is gone.\n const requestedConversationId = conversationId;\n const isStale = () => !isMountedRef.current || conversationIdRef.current !== requestedConversationId;\n\n fetch(sessionsUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({\n conversation_id: conversationId,\n agent_slug: selectedAgent.slug,\n }),\n })\n .then((res) => {\n if (isStale()) return null;\n if (!res.ok) {\n // Stale or invalid stored id (e.g. the platform was reset but the\n // browser kept an old id) — silently reset so a fresh conversation\n // is created on the next message instead of surfacing an error and\n // forcing the user to click \"New conversation\".\n updateConversationId(null);\n return null;\n }\n return res.json();\n })\n .then((data) => {\n if (!data || isStale()) return;\n // The backend resolves the session: it returns the same id when the\n // conversation still exists, or transparently creates a fresh one and\n // returns its NEW id when the stored id is stale. Adopt whatever id it\n // returns (and persist it) so we never send subsequent messages\n // against a dead conversation — which would 404 with\n // \"conversation does not exist\".\n if (typeof data.conversation_id === 'string' && data.conversation_id && data.conversation_id !== requestedConversationId) {\n updateConversationId(data.conversation_id);\n }\n if (!data.messages?.length) return;\n const restored: ChatMessage[] = data.messages.map(\n (\n m: {\n role: string;\n content: string;\n attachments?: unknown;\n tool_names?: unknown;\n tool_call_count?: unknown;\n iterations?: unknown;\n reasoning?: unknown;\n tool_call_trace?: unknown;\n transfer_chain?: unknown;\n is_truncated?: unknown;\n },\n i: number,\n ) => ({\n id: `restored-${i}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: new Date(),\n // Re-surface downloadable file chips on conversation restore for\n // both roles: agent-generated deliverables on assistant messages\n // (the [[FILE:…]] markers in content are stripped at render time by\n // ChatMessages) and user-uploaded files on user messages (so an\n // upload stays downloadable after a page reload, not just in the\n // live session where it is carried on `files`).\n attachments: parseAttachments(m.attachments),\n // Re-surface the reasoning-details affordance (\"i\" button) on\n // restored assistant messages — same fields the live `done`\n // event carries.\n toolNames: Array.isArray(m.tool_names) ? (m.tool_names as string[]) : undefined,\n toolCallCount: typeof m.tool_call_count === 'number' ? m.tool_call_count : undefined,\n iterations: typeof m.iterations === 'number' ? m.iterations : undefined,\n reasoning: typeof m.reasoning === 'string' ? m.reasoning : undefined,\n toolCallTrace: parseToolCallTrace(m.tool_call_trace),\n transferChain: parseTransferChain(m.transfer_chain),\n isTruncated: m.is_truncated === true || undefined,\n }),\n );\n setMessages(restored);\n })\n .catch(() => {\n if (isStale()) return;\n updateConversationId(null);\n });\n }, [\n conversationId,\n selectedAgent,\n apiBaseUrl,\n apiEndpoints,\n backendType,\n historyLoadedRef,\n conversationIdRef,\n isMountedRef,\n requestHeaders,\n setMessages,\n updateConversationId,\n ]);\n\n const onSwitchAgent = (agent: typeof selectedAgent) => {\n if (!agent) return;\n handleSwitchAgent(agent, () => {\n handleNewChat();\n });\n };\n\n const containerClasses = (() => {\n const base = 'filigran-chatbot';\n switch (mode) {\n case 'sidebar':\n return `${base} fixed right-0 bottom-0 flex flex-col bg-white dark:bg-[#1e1e2e] border-l border-gray-200 dark:border-white/10 z-[1200]`;\n case 'floating':\n return `${base} fixed bottom-5 right-5 flex flex-col bg-white dark:bg-[#1e1e2e] rounded-xl shadow-[0_8px_32px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_32px_rgba(0,0,0,0.4)] z-[1300] border border-gray-200 dark:border-white/10`;\n case 'fullscreen':\n return `${base} fixed right-0 bottom-0 left-0 flex flex-col bg-gray-50 dark:bg-[#161622] z-[1400]`;\n default:\n return base;\n }\n })();\n\n const containerStyle: React.CSSProperties = {\n ...cssVars,\n ...(mode === 'sidebar'\n ? { top: topOffset, width: resizable ? sidebarWidth : defaultWidth }\n : mode === 'floating'\n ? { width: FLOATING_WIDTH, height: FLOATING_HEIGHT }\n : { top: topOffset }),\n };\n\n return (\n <div className={containerClasses} style={containerStyle}>\n {mode === 'sidebar' && resizable && (\n <div onMouseDown={handleResizeStart} className=\"absolute top-0 -left-1 bottom-0 w-2 cursor-col-resize z-10 group\">\n <div className=\"absolute top-0 left-1/2 -translate-x-1/2 bottom-0 w-0.5 rounded-sm bg-[var(--chat-accent)] opacity-0 transition-opacity group-hover:opacity-100 group-active:opacity-100\" />\n </div>\n )}\n <ChatHeader\n mode={mode}\n agentName={agentName}\n agents={agents}\n selectedAgent={selectedAgent}\n transferredFrom={transferredAgent ? selectedAgent?.name : undefined}\n agentMenuOpen={agentMenuOpen}\n onAgentMenuToggle={() => setAgentMenuOpen((p) => !p)}\n onAgentMenuClose={() => setAgentMenuOpen(false)}\n onSwitchAgent={onSwitchAgent}\n modeMenuOpen={modeMenuOpen}\n onModeMenuToggle={() => setModeMenuOpen((p) => !p)}\n onModeMenuClose={() => setModeMenuOpen(false)}\n onModeChange={onModeChange}\n onNewChat={handleNewChat}\n onClose={onClose}\n logoIcon={resolvedLogo}\n agentDashboardUrl={agentDashboardUrl}\n historyEnabled={historyEnabled}\n historyMenuOpen={historyMenuOpen}\n onHistoryMenuToggle={handleHistoryMenuToggle}\n onHistoryMenuClose={() => setHistoryMenuOpen(false)}\n conversations={conversations}\n conversationsLoading={conversationsLoading}\n activeConversationId={conversationId}\n onSelectConversation={handleSelectConversation}\n onDeleteConversation={(id) => void handleDeleteConversation(id)}\n t={t}\n />\n {messages.length === 0 ? (\n <ChatWelcome firstName={firstName} logoIcon={resolvedLogo} promptSuggestions={promptSuggestions} onPromptClick={setInputValue} t={t} />\n ) : (\n <ChatMessages\n messages={messages}\n isLoading={isLoading}\n agentStatus={agentStatus}\n agentName={agentName}\n logoIcon={resolvedLogo}\n onRelativeLinkClick={onRelativeLinkClick}\n onDownloadFile={canDownload ? handleDownloadFile : undefined}\n miniGameEnabled={miniGameEnabled}\n t={t}\n />\n )}\n <ChatInput\n inputValue={inputValue}\n onInputChange={setInputValue}\n onSend={handleSendMessage}\n onStop={handleStopGenerating}\n isLoading={isLoading}\n canSteer={canSteer}\n attachedFiles={disableFileManagement ? [] : attachedFiles}\n onFileAdd={disableFileManagement ? undefined : handleFileAdd}\n onFileRemove={disableFileManagement ? undefined : (i) => setAttachedFiles((prev) => prev.filter((_, j) => j !== i))}\n onPaste={disableFileManagement ? undefined : handlePaste}\n t={t}\n mode={mode}\n separatorColor={draftBorderColor}\n />\n </div>\n );\n};\n","import type { FunctionComponent } from 'react';\nimport type { ChatToggleButtonProps } from '../types';\nimport { hexAlpha } from '../utils';\nimport { DefaultLogoIcon } from './icons';\n\nexport const ChatToggleButton: FunctionComponent<ChatToggleButtonProps> = ({\n isOpen,\n onToggle,\n label = 'Ask Assistant',\n accentColor = '#7b5cff',\n icon,\n}) => {\n const resolvedIcon = icon ?? <DefaultLogoIcon size={16} />;\n\n return (\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"filigran-chatbot inline-flex items-center gap-1.5 px-3 py-[3px] text-[0.8125rem] font-medium whitespace-nowrap rounded-md border transition-colors\"\n style={{\n borderColor: isOpen ? accentColor : hexAlpha(accentColor, 0.5),\n color: accentColor,\n backgroundColor: isOpen ? hexAlpha(accentColor, 0.1) : 'transparent',\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.borderColor = accentColor;\n e.currentTarget.style.backgroundColor = hexAlpha(accentColor, 0.1);\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.borderColor = isOpen ? accentColor : hexAlpha(accentColor, 0.5);\n e.currentTarget.style.backgroundColor = isOpen ? hexAlpha(accentColor, 0.1) : 'transparent';\n }}\n >\n <span className=\"[&>svg]:w-4 [&>svg]:h-4\">{resolvedIcon}</span>\n {label}\n </button>\n );\n};\n"],"names":["hexAlpha","hex","alpha","Math","round","toString","padStart","identity","key","findChatbotRoot","el","node","classList","contains","parentElement","document","body","PARTIAL_FILE_MARKER_RE","parseAttachments","raw","Array","isArray","out","item","a","fileId","file_id","push","filename","type","undefined","size","contentType","content_type","fileTag","file_tag","length","parseToolCallTrace","e","name","input","output","success","parseTransferChain","agent_name","agentId","agent_id","agentName","parseRestEvent","evt","ctx","action","content","st","status","thinkingContent","hasUsedTools","tools","elapsedS","elapsed_s","conversationId","conversation_id","toolNames","tool_names","toolCallCount","tool_call_count","iterations","transferAgentId","transfer_agent_id","transferAgentName","transfer_agent_name","attachments","reasoning","toolCallTrace","tool_call_trace","transferChain","transfer_chain","isTruncated","is_truncated","parseLegacyEvent","eventType","event","data","nodeId","activeNodeId","replace","usedTools","map","t","tool","chatId","parseAgUiEvent","message","stepName","delta","toolName","toolCallName","STORAGE_KEY","LEGACY_CHAT_ID_KEY","DEFAULT_MAX_TOTAL_SIZE","useChat","apiBaseUrl","apiEndpoints","backendType","agentSlug","requestHeaders","pageContext","maxFileCount","maxTotalSize","isLegacy","messages","setMessages","useState","inputValue","setInputValue","isLoading","setIsLoading","agentStatus","setAgentStatus","setConversationId","window","localStorage","getItem","attachedFiles","setAttachedFiles","transferredAgent","setTransferredAgent","legacyChatId","setLegacyChatId","historyLoadedRef","useRef","abortControllerRef","hasUsedToolsRef","conversationIdRef","pageContextRef","current","creatingSessionRef","uploadAbortRef","AbortController","effectiveMaxFileCount","Number","isFinite","floor","effectiveMaxTotalSize","getSteerUrl","singleEndpoint","steer","getUploadUrl","upload","updateConversationId","useCallback","id","setItem","removeItem","ensureConversation","async","slug","sessionsUrl","sessions","promise","res","fetch","method","headers","JSON","stringify","agent_slug","ok","json","convId","uploadSingleFile","file","signal","uploadUrl","formData","FormData","append","uploadHeaders","Object","fromEntries","entries","filter","k","toLowerCase","Error","ids","file_ids","handleFileAdd","fileList","candidates","from","tempId","crypto","randomUUID","accepted","prev","currentCount","currentSize","reduce","sum","f","slotsAvailable","sizeLeft","filtered","c","slice","newEntries","rawFile","uploadStatus","setTimeout","p","err","DOMException","handleNewChat","abort","canSteer","handlePaste","files","clipboardData","preventDefault","handleSendMessage","steerText","trim","steerUrl","optimistic","role","timestamp","Date","m","steerMessage","userMsg","assistantId","currentAssistantId","controller","fileIds","requestBody","opts","question","streaming","threadId","runId","context","state","forwardedProps","keys","serialized","buildRequestBody","parseEvent","getParser","reader","getReader","decoder","TextDecoder","buffer","accumulated","doneReceived","ensureSegment","segmentId","done","value","read","decode","stream","lines","split","pop","rawLine","line","startsWith","jsonStr","parsed","parse","segId","text","finalContent","handleStopGenerating","handleSwitchConversation","STORAGE_AGENT_KEY","parseConversation","title","updatedAt","updated_at","created_at","messageCount","message_count","SIDEBAR_WIDTH","SIDEBAR_WIDTH_STORAGE_KEY","flashTimer","originalTitle","activeHooks","stopTitleFlash","clearInterval","useAwayCompletionNotice","enabled","onComplete","isViewingChat","wasLoadingRef","startRef","useEffect","clearOnReturn","hidden","hasFocus","addEventListener","removeEventListener","max","wasLoading","now","unfocused","away","viewingChat","showMessage","setInterval","startTitleFlash","Notification","permission","tag","notifyOS","AlertTriangleIcon","className","_jsxs","xmlns","width","height","viewBox","fill","stroke","strokeWidth","strokeLinecap","strokeLinejoin","children","_jsx","d","ArrowRightLeftIcon","AttachFileIcon","BotIcon","x","y","rx","BrainIcon","CheckCircleIcon","cx","cy","r","CheckIcon","ChevronDownIcon","CloseIcon","CopyIcon","ry","DatabaseIcon","DefaultLogoIcon","DownloadIcon","points","x1","x2","y1","y2","EditIcon","ExternalLinkIcon","FileIcon","FloatingIcon","FullscreenExitIcon","FullscreenIcon","GamepadIcon","GlobeIcon","HistoryIcon","InfoIcon","MailIcon","SearchIcon","SendIcon","SidebarIcon","SparklesIcon","StopCircleIcon","TerminalIcon","TrashIcon","UserPlusIcon","WrenchIcon","XCircleIcon","Dropdown","open","onClose","anchorRef","placement","panelRef","pos","setPos","top","left","ref","handler","active","listener","target","useClickOutside","rect","getBoundingClientRect","right","bottom","portalTarget","createPortal","style","Spinner","Tooltip","show","setShow","below","setBelow","onMouseEnter","rootTop","flip","onMouseLeave","modeOptions","mode","label","getIcon","ChatHeader","agents","selectedAgent","transferredFrom","agentMenuOpen","onAgentMenuToggle","onAgentMenuClose","onSwitchAgent","modeMenuOpen","onModeMenuToggle","onModeMenuClose","onModeChange","onNewChat","logoIcon","agentDashboardUrl","historyEnabled","historyMenuOpen","onHistoryMenuToggle","onHistoryMenuClose","conversations","conversationsLoading","activeConversationId","onSelectConversation","onDeleteConversation","agentAnchorRef","modeAnchorRef","historyAnchorRef","CurrentModeIcon","onClick","agent","description","_Fragment","conv","isActive","when","iso","then","getTime","isNaN","diffMs","minutes","hours","days","toLocaleDateString","month","day","timeAgo","opt","ChatInput","onInputChange","onSend","onStop","onFileAdd","onFileRemove","onPaste","separatorColor","fileInputRef","textareaRef","isFileManagementEnabled","Boolean","hasContent","hasFilesUploading","some","canSend","hasAttachments","showSteerSend","footerText","borderTopColor","borderTopWidth","i","multiple","onChange","click","placeholder","min","scrollHeight","onKeyDown","shiftKey","rows","maxHeight","disabled","DEFAULT_MESSAGES","PREF_KEY","arcadeFont","px","INVADER_FRAMES","readPref","createInvaderGame","canvas","onMessage","maybeCtx","getContext","raf","cssW","fontPx","accent","msgIndex","letters","targetIndex","bullets","particles","shipX","cooldown","clearedAt","legFrame","legTimer","last","performance","letterY","cssH","firstAlive","alive","layout","font","measureText","widths","ch","total","b","lx","char","w","letterCenter","applySize","dpr","devicePixelRatio","setTransform","v","getComputedStyle","getPropertyValue","resolveAccent","ro","ResizeObserver","observe","requestAnimationFrame","loop","dt","dtf","targetX","abs","ang","PI","random","spd","vx","cos","vy","sin","life","splice","update","clearRect","fillStyle","textBaseline","globalAlpha","l","fillText","fillRect","frame","ox","SPRITE_W","row","draw","cancelAnimationFrame","disconnect","ChatWaitingGame","useMemo","reducedMotion","reduced","setReduced","matchMedia","matches","mql","addListener","removeListener","usePrefersReducedMotion","minigameOn","setMinigameOn","setMsgIndex","canvasRef","rootRef","playMode","scroller","oy","overflowY","clientHeight","findScrollParent","scrollTop","animation","next","on","writePref","cleanReasoningText","ThinkingTextBubble","isOverflowing","setIsOverflowing","cleaned","formatElapsed","seconds","s","ChatThinking","miniGameEnabled","StatusIcon","showDots","rawNames","lower","n","count","includes","display","toUpperCase","unique","Set","consultName","checkCount","fetchCount","targetName","resolveStatusVisual","showElapsed","stalled","delayMs","setStalled","prevSignalRef","signalChanged","clearTimeout","useStalled","showGame","delay","isRelativeHref","href","test","MarkdownMessage","onRelativeLinkClick","copiedBlock","setCopiedBlock","processedContent","indexOf","splitCells","endsWith","cells","isDelimiterRow","alignOf","cell","fenceRe","fenceChar","fenceLen","listMarkerRe","fenceMatch","match","run","header","delim","markerMatch","offset","trimStart","indent","repeat","headerCols","delimCells","aligns","join","normalizeMarkdownTables","markupLang","openerIdx","maxRun","nestedCount","lastBareFence","fence","om","cm","hardenNestedCodeFences","Markdown","remarkPlugins","remarkGfm","components","code","exec","codeStr","String","handleCopyCode","navigator","clipboard","writeText","ul","ol","blockquote","internalHref","url","URL","location","protocol","origin","pathname","search","hash","toInternalHref","routeInternally","openInNewTab","rel","h1","h2","h3","table","th","td","prettyTraceValue","toolDisplayName","ToolCallRow","entry","index","expanded","setExpanded","inputDisplay","outputDisplay","hasInput","ReasoningDetailsDialog","msg","hostRef","closeButtonRef","dialogRef","root","setRoot","dialog","focusable","querySelectorAll","first","activeElement","focus","previouslyFocused","HTMLElement","preventScroll","totalCalls","transfers","trace","summaryParts","stopPropagation","tn","tr","fileExtensionLabel","dot","lastIndexOf","ext","ChatMessages","onDownloadFile","messagesEndRef","toolDetailMsgId","setToolDetailMsgId","scrollIntoView","behavior","thinkingLen","renderAttachmentCard","att","isWorking","sizeLabel","bytes","toFixed","buildAssistantBlocks","loading","parts","re","lastIndex","tail","splitFileMarkers","attByFileId","Map","used","blocks","forEach","part","get","add","has","renderFileChip","buildUserFileBlocks","seen","lastAssistantIndex","isAssistant","isEmpty","isStreamingMessage","ChatWelcome","firstName","promptSuggestions","onPromptClick","fontFamily","prompt","DEFAULT_SUGGESTIONS","ChatPanel","topOffset","user","accentColor","draftBorderColor","resizable","onWidthChange","onResizeStart","onResizeEnd","disableFileManagement","onDownloadError","pushContentSelector","notifyOnComplete","onTaskComplete","setModeMenuOpen","setAgentMenuOpen","handleSwitchAgent","setAgents","setSelectedAgent","savedSlug","find","catch","onSwitch","useAgents","refreshConversations","deleteConversation","setConversations","setConversationsLoading","history","rawList","encodeURIComponent","useConversations","setHistoryMenuOpen","sidebarWidth","handleResizeStart","defaultWidth","isResizing","setSidebarWidth","stored","parseInt","setIsResizing","isResizingRef","sidebarWidthRef","onWidthChangeRef","onResizeEndRef","handleMouseMove","newWidth","innerWidth","clientX","maxWidth","clamped","handleMouseUp","cursor","userSelect","handleWindowResize","useSidebarResize","pushWidth","documentElement","setProperty","contentElement","querySelector","originalPaddingRight","paddingRight","originalTransition","transition","resolvedLogo","panel","checkVisibility","getClientRects","downloadPathProvided","download","canDownload","handleDownloadFile","credentials","blob","objectUrl","createObjectURL","link","createElement","appendChild","remove","revokeObjectURL","cssVars","isMountedRef","requestedConversationId","isStale","restored","containerClasses","base","containerStyle","onMouseDown","handleDeleteConversation","_","j","ChatToggleButton","isOpen","onToggle","icon","resolvedIcon","borderColor","color","backgroundColor","currentTarget"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../src/utils/index.ts","../../src/hooks/protocols/parseRestEvent.ts","../../src/hooks/protocols/parseLegacyEvent.ts","../../src/hooks/protocols/parseAgUiEvent.ts","../../src/hooks/useChat.ts","../../src/hooks/useAgents.ts","../../src/hooks/useConversations.ts","../../src/hooks/useSidebarResize.ts","../../src/hooks/useAwayCompletionNotice.ts","../../src/hooks/useComposerExtras.ts","../../src/hooks/useAgentSuggestions.ts","../../src/components/icons/AlertTriangleIcon.tsx","../../src/components/icons/ArrowRightLeftIcon.tsx","../../src/components/icons/AttachFileIcon.tsx","../../src/components/icons/BotIcon.tsx","../../src/components/icons/BrainIcon.tsx","../../src/components/icons/CheckCircleIcon.tsx","../../src/components/icons/CheckIcon.tsx","../../src/components/icons/ChevronDownIcon.tsx","../../src/components/icons/CloseIcon.tsx","../../src/components/icons/CopyIcon.tsx","../../src/components/icons/DatabaseIcon.tsx","../../src/components/icons/DefaultLogoIcon.tsx","../../src/components/icons/DownloadIcon.tsx","../../src/components/icons/EditIcon.tsx","../../src/components/icons/ExternalLinkIcon.tsx","../../src/components/icons/FileIcon.tsx","../../src/components/icons/FloatingIcon.tsx","../../src/components/icons/FullscreenExitIcon.tsx","../../src/components/icons/FullscreenIcon.tsx","../../src/components/icons/GamepadIcon.tsx","../../src/components/icons/GlobeIcon.tsx","../../src/components/icons/HistoryIcon.tsx","../../src/components/icons/ImageIcon.tsx","../../src/components/icons/InfoIcon.tsx","../../src/components/icons/MailIcon.tsx","../../src/components/icons/MaximizeIcon.tsx","../../src/components/icons/MicIcon.tsx","../../src/components/icons/SearchIcon.tsx","../../src/components/icons/SendIcon.tsx","../../src/components/icons/SidebarIcon.tsx","../../src/components/icons/SparklesIcon.tsx","../../src/components/icons/StopCircleIcon.tsx","../../src/components/icons/TerminalIcon.tsx","../../src/components/icons/ThumbsDownIcon.tsx","../../src/components/icons/ThumbsUpIcon.tsx","../../src/components/icons/TrashIcon.tsx","../../src/components/icons/UserPlusIcon.tsx","../../src/components/icons/WrenchIcon.tsx","../../src/components/icons/XCircleIcon.tsx","../../src/components/Dropdown.tsx","../../src/hooks/useClickOutside.ts","../../src/components/Spinner.tsx","../../src/components/Tooltip.tsx","../../src/components/ChatHeader.tsx","../../src/hooks/useDictation.ts","../../src/components/ContextUsageIndicator.tsx","../../src/components/PromptPicker.tsx","../../src/components/QuotaIndicator.tsx","../../src/components/ChatInput.tsx","../../src/components/ChatApprovalPrompt.tsx","../../src/components/ChatImage.tsx","../../src/components/ChatWaitingGame.tsx","../../src/components/ChatThinking.tsx","../../src/components/MarkdownMessage.tsx","../../src/components/ReasoningDetailsDialog.tsx","../../src/components/ChatMessages.tsx","../../src/components/ChatWelcome.tsx","../../src/components/ConversationSidebar.tsx","../../src/components/ChatPanel.tsx","../../src/components/ChatToggleButton.tsx"],"sourcesContent":["export function hexAlpha(hex: string, alpha: number): string {\n const a = Math.round(alpha * 255)\n .toString(16)\n .padStart(2, '0');\n return `${hex}${a}`;\n}\n\n/**\n * Markdown has no native support for nesting fenced code blocks of the same\n * length: per the CommonMark spec, the first inner ``` closes the outer block,\n * so everything after it renders *outside* the code block. LLMs constantly hit\n * this — when an agent shows a prompt or a full markdown document inside a\n * ```markdown … ``` fence, that document's own ``` fences shatter the snippet\n * into alternating code / prose fragments.\n *\n * The robust, spec-compliant fix is to make the *outer* fence longer than any\n * fence it contains: a 4-backtick fence is only closed by a run of ≥4\n * backticks, so all inner 3-backtick fences become literal content and the\n * whole document renders as one clean, copyable code block.\n *\n * We act only on the unambiguous, dominant case — a 3-backtick opener whose\n * info string is a markup language (markdown / md / mdx / markup) that actually\n * contains nested fences — so already-correct markdown is never rewritten\n * (a correctly authored nested block already uses a 4+-backtick opener, which\n * we skip).\n */\nexport function hardenNestedCodeFences(raw: string): string {\n if (!raw) return raw;\n const lines = raw.split('\\n');\n const fenceRe = /^(\\s*)(`{3,})(.*)$/;\n const markupLang = /^(markdown|md|mdx|markup)\\b/i;\n\n let openerIdx = -1;\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].match(fenceRe);\n if (m && m[2].length === 3 && markupLang.test(m[3].trim())) {\n openerIdx = i;\n break;\n }\n }\n if (openerIdx === -1) return raw;\n\n let maxRun = 3;\n let nestedCount = 0;\n let lastBareFence = -1;\n for (let i = openerIdx + 1; i < lines.length; i++) {\n const m = lines[i].match(fenceRe);\n if (!m) continue;\n nestedCount++;\n maxRun = Math.max(maxRun, m[2].length);\n if (m[3].trim() === '') lastBareFence = i;\n }\n if (nestedCount === 0) return raw;\n\n const fence = '`'.repeat(Math.max(maxRun + 1, 4));\n const om = lines[openerIdx].match(fenceRe)!;\n lines[openerIdx] = `${om[1]}${fence}${om[3]}`;\n if (lastBareFence > openerIdx) {\n const cm = lines[lastBareFence].match(fenceRe)!;\n lines[lastBareFence] = `${cm[1]}${fence}`;\n }\n return lines.join('\\n');\n}\n\n/**\n * GFM renders a pipe table only when the delimiter row (`|---|---|`) has the\n * SAME number of columns as the header row. LLMs frequently miscount (e.g. a\n * 4-column header followed by a 3-column delimiter), and a server-side guard can\n * corrupt the delimiter — in either case the whole table silently degrades to\n * raw `| … |` text. This repairs a mismatched delimiter row to the header's\n * column count (preserving any alignment colons) so the table renders.\n *\n * It only rewrites a delimiter that is ACTUALLY mismatched, so already-valid\n * tables are never touched. Fenced code blocks are skipped, and setext headings\n * (underlines with no `|`) are never mistaken for a table.\n */\nexport function normalizeMarkdownTables(raw: string): string {\n if (!raw || raw.indexOf('|') === -1) return raw;\n const lines = raw.split('\\n');\n\n // Splits on unescaped `|` only. A manual walk (rather than a negative\n // lookbehind, which is unsupported on older engines and would throw at parse\n // time in an untranspiled ESNext bundle) keeps `\\|` inside a cell intact.\n const splitCells = (row: string): string[] => {\n let s = row.trim();\n if (s.startsWith('|')) s = s.slice(1);\n if (s.endsWith('|')) s = s.slice(0, -1);\n const cells: string[] = [];\n let current = '';\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (ch === '\\\\' && i + 1 < s.length) {\n current += ch + s[i + 1];\n i++;\n } else if (ch === '|') {\n cells.push(current);\n current = '';\n } else {\n current += ch;\n }\n }\n cells.push(current);\n return cells;\n };\n const isDelimiterRow = (row: string): boolean => row.includes('|') && /^\\s*\\|?\\s*:?-+:?\\s*(\\|\\s*:?-+:?\\s*)*\\|?\\s*$/.test(row);\n // Emit the canonical 3-hyphen delimiter form. remark-gfm accepts a single\n // hyphen, but `---` (with optional alignment colons) is the portable form\n // every Markdown renderer agrees on, so prefer it.\n const alignOf = (cell: string): string => {\n const c = cell.trim();\n const left = c.startsWith(':');\n const right = c.endsWith(':');\n return left && right ? ':---:' : right ? '---:' : left ? ':---' : '---';\n };\n\n // Track both the fence character AND its run length: per CommonMark a closing\n // fence must use the same character and be at least as long as the opener, so\n // a shorter run (```) must not close a longer one (````), and a closing fence\n // carries no info string. Optional blockquote / list-item markers are allowed\n // before the run so fences nested in those containers (e.g. `- ```) are still\n // recognised and the table inside them is left untouched.\n const fenceRe = /^\\s*(?:(?:>\\s?)|(?:[-*+]\\s+)|(?:\\d{1,9}[.)]\\s+))*(`{3,}|~{3,})(.*)$/;\n let fenceChar: string | null = null;\n let fenceLen = 0;\n // A pipe-table header can share its line with a list-item marker (e.g.\n // `- | a | b |`). Strip a leading list marker before counting columns so the\n // count matches the cells GFM sees inside the list item, not the marker.\n const listMarkerRe = /^\\s*(?:[-*+]\\s+|\\d{1,9}[.)]\\s+)/;\n for (let i = 0; i < lines.length - 1; i++) {\n const fenceMatch = lines[i].match(fenceRe);\n if (fenceMatch) {\n const run = fenceMatch[1];\n if (fenceChar === null) {\n fenceChar = run[0];\n fenceLen = run.length;\n } else if (run[0] === fenceChar && run.length >= fenceLen && fenceMatch[2].trim() === '') {\n fenceChar = null;\n fenceLen = 0;\n }\n continue;\n }\n if (fenceChar !== null) continue;\n\n const header = lines[i];\n const delim = lines[i + 1];\n if (!header.includes('|') || isDelimiterRow(header) || !isDelimiterRow(delim)) continue;\n\n // A table can start on a list-item line, so both the cells and the\n // delimiter's alignment live AFTER the marker. Anchor the rewritten\n // delimiter to that content offset (padding the marker width with spaces) so\n // it stays a continuation line of the list item: GFM drops a table whose\n // delimiter dedents away from its header. Without a marker this is just the\n // header's leading whitespace, so top-level tables are emitted unchanged.\n const markerMatch = header.match(listMarkerRe);\n const offset = markerMatch ? markerMatch[0].length : header.length - header.trimStart().length;\n const indent = markerMatch ? ' '.repeat(offset) : header.slice(0, offset);\n\n const headerCols = splitCells(header.slice(offset)).length;\n const delimCells = splitCells(delim);\n if (headerCols < 2 || delimCells.length === headerCols) continue;\n\n const aligns: string[] = [];\n for (let c = 0; c < headerCols; c++) aligns.push(delimCells[c] ? alignOf(delimCells[c]) : '---');\n lines[i + 1] = `${indent}| ${aligns.join(' | ')} |`;\n }\n return lines.join('\\n');\n}\n\n/**\n * When the ENTIRE message is a raw JSON object/array (and not already fenced),\n * wrap it in a ```json fence so it renders as a proper, copyable code block\n * instead of one long line of mangled prose — markdown collapses the newlines\n * and eats the `*`/`_` inside string values otherwise.\n *\n * Deliberately conservative: it parses the payload first, so a message that\n * merely *starts* with `{` (e.g. prose about an object) is left untouched.\n */\nexport function wrapBareJson(raw: string): string {\n const trimmed = raw.trim();\n if ((!trimmed.startsWith('{') && !trimmed.startsWith('[')) || trimmed.startsWith('```')) return raw;\n try {\n JSON.parse(trimmed);\n return '```json\\n' + trimmed + '\\n```';\n } catch {\n return raw;\n }\n}\n\n/**\n * Models routinely emit markdown images whose alt text is the whole generation\n * prompt, spread over several lines. CommonMark has no multi-line `![…](…)`, so\n * the image syntax breaks apart and the prompt renders as literal paragraphs\n * followed by a stray link.\n *\n * This collapses the alt text of such an image onto a single line (whitespace\n * runs → one space) so the parser emits a real `<img>` node. Single-line images\n * are returned byte-identical, and the URL is never rewritten — the URL may\n * contain anything, and only the alt text is at fault.\n *\n * The URL part excludes `)` and whitespace, so a link title\n * (`![alt](url \"title\")`) or a parenthesised URL is left alone rather than\n * mis-parsed.\n */\nexport function normalizeImageMarkdown(raw: string): string {\n if (!raw.includes('![')) return raw;\n return raw.replace(/!\\[([^\\]]*?\\n[^\\]]*?)\\]\\(([^)\\s]+)\\)/g, (_match, alt: string, url: string) => {\n const flat = alt.replace(/\\s+/g, ' ').trim();\n return `![${flat}](${url})`;\n });\n}\n\n/** Schemes react-markdown's own sanitizer allows, minus the `data:` special case below. */\nconst SAFE_URL_PROTOCOLS = new Set(['http', 'https', 'mailto', 'tel']);\n\n/**\n * URL sanitizer for react-markdown that additionally preserves `data:image/*`\n * URIs.\n *\n * react-markdown v9+ ships a default `urlTransform` allowing only\n * http / https / mailto / tel. Agents with a code interpreter return charts as\n * `data:image/png;base64,…`, which that default strips — leaving a broken\n * `<img>`. Every other scheme (notably `javascript:` / `vbscript:`) is still\n * blocked, and non-image `data:` URIs are blocked too, so this widens the\n * allow-list by exactly one safe, inert case.\n */\nexport function markdownUrlTransform(url: string): string {\n const colon = url.indexOf(':');\n if (colon < 0) return url; // relative URL — always safe\n\n const slash = url.indexOf('/');\n const question = url.indexOf('?');\n const hash = url.indexOf('#');\n\n // A `/`, `?` or `#` before the colon means this is a path, not a scheme\n // (e.g. `./a:b`), so there is nothing to sanitize.\n if ((slash > -1 && colon > slash) || (question > -1 && colon > question) || (hash > -1 && colon > hash)) {\n return url;\n }\n\n const protocol = url.slice(0, colon).toLowerCase();\n if (SAFE_URL_PROTOCOLS.has(protocol)) return url;\n if (protocol === 'data' && /^data:image\\//i.test(url)) return url;\n return '';\n}\n\nexport const identity = (key: string) => key;\n\n/**\n * Nearest chatbot panel root (`.filigran-chatbot`) for portal-based overlays\n * (tooltips, dropdowns, dialogs), so they stay inside the panel's stacking\n * context instead of competing with the host app's z-indexes. Falls back to\n * `document.body` when rendered outside a panel.\n */\nexport function findChatbotRoot(el: HTMLElement | null): HTMLElement {\n let node = el;\n while (node) {\n if (node.classList.contains('filigran-chatbot')) return node;\n node = node.parentElement;\n }\n return document.body;\n}\n\n/**\n * Compact count for the composer's status readouts: 1200 → \"1.2k\", so neither\n * the quota nor the context gauge can widen the toolbar as the numbers grow.\n */\nexport function compactCount(n: number): string {\n if (n < 1000) return `${n}`;\n if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0).replace(/\\.0$/, '')}k`;\n return `${(n / 1_000_000).toFixed(1).replace(/\\.0$/, '')}M`;\n}\n\n/**\n * Compact relative-time label for the conversation history menu\n * (\"just now\", \"5m ago\", \"3h ago\", \"2d ago\", then a short date).\n * Returns an empty string for missing/unparseable timestamps so the row\n * simply omits the label instead of showing \"Invalid Date\".\n */\nexport function timeAgo(iso: string | undefined, t: (key: string) => string): string {\n if (!iso) return '';\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return '';\n const diffMs = Date.now() - then;\n const minutes = Math.floor(diffMs / 60_000);\n if (minutes < 1) return t('just now');\n if (minutes < 60) return `${minutes}${t('m ago')}`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}${t('h ago')}`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}${t('d ago')}`;\n return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\n/** Matches a complete `[[FILE:<id>]]` deliverable marker agents embed in prose. */\nconst FILE_MARKER_RE = /\\[\\[FILE:[^\\]]+\\]\\]/g;\n\n/**\n * Matches an INCOMPLETE marker at the very end of the string. SSE streams can\n * split a `[[FILE:<id>]]` token before the closing `]]` arrives, so the tail\n * may be `[[FILE`, `[[FILE:`, `[[FILE:abc`, or `[[FILE:abc]` mid-stream. We\n * anchor on the literal `[[FILE` prefix (which is vanishingly unlikely to\n * appear legitimately at the end of prose) so it never clips real content.\n */\nconst PARTIAL_FILE_MARKER_RE = /\\[\\[FILE(?::[^\\]]*)?\\]?$/;\n\n/**\n * Strip the `[[FILE:<id>]]` markers an agent embeds in its reply to point at\n * generated files. The actual files render as separate download chips, so the\n * raw markers must be removed from the prose. Complete markers are removed\n * anywhere; an incomplete marker at the end is also removed so a partially\n * streamed token never flickers as raw `[[FILE:...` text.\n *\n * When no marker is present the content is returned **untouched** — we must\n * not trim or collapse blank lines on ordinary prose, which would clobber\n * intentional leading/trailing whitespace (e.g. indented Markdown / code).\n * Whitespace is only normalized when a marker was actually removed.\n *\n * Applied to assistant content only — user-typed text is never touched, so a\n * user who literally types `[[FILE:x]]` still sees their own text.\n */\nexport function stripFileMarkers(content: string): string {\n if (!content) return content;\n const stripped = content.replace(FILE_MARKER_RE, '').replace(PARTIAL_FILE_MARKER_RE, '');\n if (stripped === content) return content;\n return stripped\n .replace(/[ \\t]+\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n/** An ordered piece of assistant content: prose text or a file marker. */\nexport type FileMarkerPart = { type: 'text'; value: string } | { type: 'file'; fileId: string };\n\n/**\n * Split assistant content into ordered text/file parts around complete\n * `[[FILE:<id>]]` markers, so the renderer can place each download card at the\n * marker's source position (preserving interleaved order). A trailing\n * incomplete marker (an SSE token split mid-stream) is removed from the final\n * text part so it never shows as raw `[[FILE:...` text.\n */\nexport function splitFileMarkers(content: string): FileMarkerPart[] {\n if (!content) return [];\n const parts: FileMarkerPart[] = [];\n const re = /\\[\\[FILE:([^\\]]+)\\]\\]/g;\n let lastIndex = 0;\n let match: RegExpExecArray | null = re.exec(content);\n while (match !== null) {\n if (match.index > lastIndex) {\n parts.push({ type: 'text', value: content.slice(lastIndex, match.index) });\n }\n parts.push({ type: 'file', fileId: match[1] });\n lastIndex = re.lastIndex;\n match = re.exec(content);\n }\n const tail = content.slice(lastIndex).replace(PARTIAL_FILE_MARKER_RE, '');\n if (tail) parts.push({ type: 'text', value: tail });\n return parts;\n}\n","import type {\n ChatAttachment,\n ChatContextBreakdown,\n ChatContextUsage,\n ToolApprovalProposal,\n ToolCallTraceEntry,\n TransferChainEntry,\n} from '../../types';\nimport type { ParsedAction, ProtocolContext } from './types';\n\n/** Wire key → the breakdown field it populates. */\nconst BREAKDOWN_KEYS: ReadonlyArray<[string, keyof ChatContextBreakdown]> = [\n ['system', 'system'],\n ['tools', 'tools'],\n ['dynamic_tools', 'dynamicTools'],\n ['summary', 'summary'],\n ['conversation', 'conversation'],\n ['tool_results', 'toolResults'],\n];\n\n/**\n * Normalize the optional `context_breakdown` object.\n *\n * Only positive numbers for keys we know how to label survive: an unlabelled\n * bucket cannot be rendered, and a zero one is noise in a list meant to show\n * where the context actually went. Returns `undefined` when nothing usable is\n * left, so the gauge keeps its number and simply has no detail to open.\n */\nfunction parseBreakdown(raw: unknown): ChatContextBreakdown | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const src = raw as Record<string, unknown>;\n const out: ChatContextBreakdown = {};\n let any = false;\n for (const [wireKey, field] of BREAKDOWN_KEYS) {\n const value = src[wireKey];\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) {\n out[field] = value;\n any = true;\n }\n }\n return any ? out : undefined;\n}\n\n/**\n * Read the context-window occupancy carried by a progress or `done` frame.\n *\n * Both halves are required and the window must be positive: a token count with\n * no window to measure it against is not a ratio, and a zero window would make\n * one out of a division by zero. Returns `undefined` for anything else, so a\n * backend that reports nothing simply leaves the gauge as it was.\n */\nexport function parseContextUsage(evt: Record<string, unknown>): ChatContextUsage | undefined {\n const used = evt.context_tokens;\n const limit = evt.context_window;\n if (typeof used !== 'number' || typeof limit !== 'number') return undefined;\n if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0 || used < 0) return undefined;\n const breakdown = parseBreakdown(evt.context_breakdown);\n return breakdown ? { used, limit, breakdown } : { used, limit };\n}\n\n/**\n * Normalize the raw `attachments` array from a backend `done` event into\n * typed {@link ChatAttachment} objects. Defensive: skips non-object entries\n * and entries without a `file_id`. Returns `undefined` when there is nothing\n * renderable so the `done` action stays lean for backends without #810.\n */\nexport function parseAttachments(raw: unknown): ChatAttachment[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: ChatAttachment[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const a = item as Record<string, unknown>;\n const fileId = a.file_id;\n if (typeof fileId !== 'string' || !fileId) continue;\n out.push({\n fileId,\n filename: typeof a.filename === 'string' ? a.filename : 'file',\n type: typeof a.type === 'string' ? a.type : undefined,\n size: typeof a.size === 'number' ? a.size : undefined,\n contentType: typeof a.content_type === 'string' ? a.content_type : undefined,\n fileTag: a.file_tag === 'working_file' ? 'working_file' : 'download_file',\n });\n }\n return out.length > 0 ? out : undefined;\n}\n\n/**\n * Normalize the raw `tool_call_trace` array (from a `done` event or restored\n * session metadata) into typed {@link ToolCallTraceEntry} objects. Defensive:\n * skips entries without a `name`. Returns `undefined` when empty so the\n * reasoning-details dialog falls back to the flat tool-name list.\n */\nexport function parseToolCallTrace(raw: unknown): ToolCallTraceEntry[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: ToolCallTraceEntry[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const e = item as Record<string, unknown>;\n if (typeof e.name !== 'string' || !e.name) continue;\n out.push({\n name: e.name,\n input: typeof e.input === 'string' ? e.input : undefined,\n output: typeof e.output === 'string' ? e.output : undefined,\n // Only a boolean is honored; a missing/malformed value defaults to\n // success so unknown states never render a false failure icon.\n success: typeof e.success === 'boolean' ? e.success : true,\n });\n }\n return out.length > 0 ? out : undefined;\n}\n\n/**\n * Normalize the raw `transfer_chain` array (from a `done` event or restored\n * session metadata) into typed {@link TransferChainEntry} objects.\n */\nexport function parseTransferChain(raw: unknown): TransferChainEntry[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: TransferChainEntry[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const e = item as Record<string, unknown>;\n if (typeof e.agent_name !== 'string' || !e.agent_name) continue;\n out.push({\n agentId: typeof e.agent_id === 'string' ? e.agent_id : '',\n agentName: e.agent_name,\n });\n }\n return out.length > 0 ? out : undefined;\n}\n\n/**\n * Normalize the `proposals` array of an `approval_required` event.\n *\n * Only `tool_call_id` is load-bearing — it is what a decision is keyed on, so\n * an entry without one could never be answered and is dropped. Everything else\n * degrades: a proposal with no name still renders under a placeholder rather\n * than vanishing, because a call silently hidden from the reviewer is a call\n * that stalls the turn with nothing on screen to explain it.\n *\n * Returns `undefined` when nothing decidable is left, so the consumer can treat\n * an unusable pause as one it must not claim to have prompted for.\n */\nexport function parseToolApprovalProposals(raw: unknown): ToolApprovalProposal[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: ToolApprovalProposal[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const p = item as Record<string, unknown>;\n const toolCallId = p.tool_call_id;\n if (typeof toolCallId !== 'string' || !toolCallId) continue;\n const args = p.arguments;\n const schema = p.input_schema;\n out.push({\n toolCallId,\n toolName: typeof p.tool_name === 'string' && p.tool_name ? p.tool_name : 'unknown tool',\n toolDescription: typeof p.tool_description === 'string' ? p.tool_description : undefined,\n arguments: args && typeof args === 'object' && !Array.isArray(args) ? (args as Record<string, unknown>) : {},\n inputSchema: schema && typeof schema === 'object' && !Array.isArray(schema) ? (schema as Record<string, unknown>) : undefined,\n source: typeof p.source === 'string' ? p.source : undefined,\n });\n }\n return out.length > 0 ? out : undefined;\n}\n\n/**\n * Parse an XTM One (REST) SSE event into a normalized action.\n */\nexport function parseRestEvent(evt: Record<string, unknown>, ctx: ProtocolContext): ParsedAction {\n const type = evt.type as string | undefined;\n\n if (type === 'error') {\n return { action: 'error', content: (evt.content as string) || '' };\n }\n\n if (type === 'status') {\n const st = evt.status as string;\n if (st === 'tool_done' || st === 'wind_down') {\n return { action: 'noop' };\n }\n if (st === 'streaming') {\n return { action: 'status', status: 'streaming' };\n }\n if (st === 'thinking_text') {\n return { action: 'status', status: 'thinking_text', thinkingContent: evt.content as string };\n }\n if (st === 'tool_start') {\n ctx.hasUsedTools = true;\n return { action: 'status', status: 'tool_start', tools: evt.tools as string[] | undefined };\n }\n if (st === 'tool_heartbeat') {\n // Liveness signal during a long tool execution (background tasks,\n // consults, big integration calls): carries the elapsed seconds but\n // no new semantic state — the consumer must keep its current label.\n return {\n action: 'status',\n status: 'tool_heartbeat',\n tools: evt.tools as string[] | undefined,\n elapsedS: typeof evt.elapsed_s === 'number' ? evt.elapsed_s : undefined,\n };\n }\n // Context occupancy rides on the per-iteration `thinking` frame, so the\n // gauge climbs during a long turn. Read before the `analyzing` relabel\n // below, which rewrites the status but not what the frame carries.\n const contextUsage = parseContextUsage(evt);\n if (st === 'thinking' && ctx.hasUsedTools) {\n return { action: 'status', status: 'analyzing', contextUsage };\n }\n return { action: 'status', status: st, tools: evt.tools as string[] | undefined, contextUsage };\n }\n\n // The turn has paused on a gated tool call. Deliberately NOT terminal: the\n // stream stays open (kept warm by SSE `: keepalive` comment lines, which the\n // reader below drops as non-`data:` lines) and the rest of the turn arrives\n // on it once a decision is POSTed back.\n if (type === 'approval_required') {\n const proposals = parseToolApprovalProposals(evt.proposals);\n if (!proposals) return { action: 'noop' };\n return {\n action: 'approval_required',\n proposals,\n conversationId: typeof evt.conversation_id === 'string' ? evt.conversation_id : undefined,\n };\n }\n\n if (type === 'stream') {\n return { action: 'stream', content: evt.content as string };\n }\n\n if (type === 'done') {\n return {\n action: 'done',\n content: evt.content as string,\n conversationId: evt.conversation_id as string | undefined,\n toolNames: evt.tool_names as string[] | undefined,\n toolCallCount: evt.tool_call_count as number | undefined,\n iterations: evt.iterations as number | undefined,\n transferAgentId: evt.transfer_agent_id as string | undefined,\n transferAgentName: evt.transfer_agent_name as string | undefined,\n attachments: parseAttachments(evt.attachments),\n reasoning: typeof evt.reasoning === 'string' ? evt.reasoning : undefined,\n toolCallTrace: parseToolCallTrace(evt.tool_call_trace),\n transferChain: parseTransferChain(evt.transfer_chain),\n isTruncated: evt.is_truncated === true || undefined,\n contextUsage: parseContextUsage(evt),\n };\n }\n\n return { action: 'noop' };\n}\n","import type { ParsedAction, ProtocolContext } from './types';\n\n/**\n * Parse a Flowise-style SSE event into a normalized action.\n */\nexport function parseLegacyEvent(evt: Record<string, unknown>, ctx: ProtocolContext): ParsedAction {\n const eventType = evt.event as string | undefined;\n\n if (eventType === 'nextAgentFlow') {\n const data = evt.data as Record<string, unknown> | undefined;\n const nodeId = data?.nodeId as string | undefined;\n if (data?.status === 'INPROGRESS' && nodeId) {\n ctx.activeNodeId = nodeId;\n }\n return { action: 'noop' };\n }\n\n if (eventType === 'start') {\n return { action: 'noop' };\n }\n\n if (eventType === 'token') {\n const tokenData = ((evt.data as string) ?? '').replace(/<br\\s*\\/?>/g, '\\n');\n return { action: 'stream', content: tokenData };\n }\n\n if (eventType === 'agentReasoning') {\n const reasoning = evt.data as Record<string, unknown> | undefined;\n const usedTools = reasoning?.usedTools as Array<{ tool: string }> | undefined;\n if (usedTools?.length) {\n ctx.hasUsedTools = true;\n return { action: 'status', status: 'tool_start', tools: usedTools.map((t) => t.tool) };\n }\n if (ctx.hasUsedTools) {\n return { action: 'status', status: 'analyzing' };\n }\n return { action: 'status', status: 'thinking' };\n }\n\n if (eventType === 'usedTools') {\n ctx.hasUsedTools = true;\n const data = evt.data as Array<{ tool: string }> | undefined;\n const toolNames = Array.isArray(data) ? data.map((t) => t.tool) : [];\n return { action: 'status', status: 'tool_start', tools: toolNames };\n }\n\n if (eventType === 'metadata') {\n const data = evt.data as Record<string, unknown> | undefined;\n const chatId = data?.chatId as string | undefined;\n if (chatId) {\n return { action: 'set_chat_id', chatId };\n }\n return { action: 'noop' };\n }\n\n if (eventType === 'error') {\n return { action: 'error', content: (evt.data as string) || '' };\n }\n\n if (eventType === 'end') {\n return { action: 'done', content: '' };\n }\n\n return { action: 'noop' };\n}\n","import type { ParsedAction, ProtocolContext } from './types';\n\n/**\n * AG-UI protocol event types.\n * @see https://github.com/ag-ui-protocol/ag-ui\n */\n\n/**\n * Parse an AG-UI protocol SSE event into a normalized action.\n *\n * AG-UI uses a Start/Content/End lifecycle for messages and tool calls.\n * We map these to the same internal actions used by the other protocols.\n */\nexport function parseAgUiEvent(evt: Record<string, unknown>, ctx: ProtocolContext): ParsedAction {\n const type = evt.type as string | undefined;\n\n // --- Run lifecycle ---\n\n if (type === 'RUN_STARTED') {\n return { action: 'status', status: 'thinking' };\n }\n\n if (type === 'RUN_FINISHED') {\n return { action: 'done', content: '' };\n }\n\n if (type === 'RUN_ERROR') {\n return { action: 'error', content: (evt.message as string) || 'Unknown error' };\n }\n\n // --- Step lifecycle ---\n\n if (type === 'STEP_STARTED') {\n const stepName = evt.stepName as string | undefined;\n return { action: 'status', status: stepName || 'thinking' };\n }\n\n if (type === 'STEP_FINISHED') {\n return { action: 'noop' };\n }\n\n // --- Text message streaming ---\n\n if (type === 'TEXT_MESSAGE_START') {\n return { action: 'status', status: 'streaming' };\n }\n\n if (type === 'TEXT_MESSAGE_CONTENT') {\n const delta = evt.delta as string;\n if (delta) {\n return { action: 'stream', content: delta };\n }\n return { action: 'noop' };\n }\n\n if (type === 'TEXT_MESSAGE_END') {\n return { action: 'noop' };\n }\n\n // TEXT_MESSAGE_CHUNK is a convenience event that combines Start+Content+End\n if (type === 'TEXT_MESSAGE_CHUNK') {\n const delta = evt.delta as string | undefined;\n if (delta) {\n return { action: 'stream', content: delta };\n }\n return { action: 'noop' };\n }\n\n // --- Tool call lifecycle ---\n\n if (type === 'TOOL_CALL_START') {\n ctx.hasUsedTools = true;\n const toolName = evt.toolCallName as string | undefined;\n return { action: 'status', status: 'tool_start', tools: toolName ? [toolName] : [] };\n }\n\n if (type === 'TOOL_CALL_ARGS') {\n // Tool arguments streaming — no UI equivalent, skip\n return { action: 'noop' };\n }\n\n if (type === 'TOOL_CALL_END') {\n return { action: 'status', status: 'analyzing' };\n }\n\n if (type === 'TOOL_CALL_RESULT') {\n // Tool result — no direct UI mapping, skip\n return { action: 'noop' };\n }\n\n if (type === 'TOOL_CALL_CHUNK') {\n // Convenience form — treat like TOOL_CALL_START if it has a name\n const toolName = evt.toolCallName as string | undefined;\n if (toolName) {\n ctx.hasUsedTools = true;\n return { action: 'status', status: 'tool_start', tools: [toolName] };\n }\n return { action: 'noop' };\n }\n\n // --- Reasoning / thinking ---\n\n if (type === 'REASONING_START' || type === 'REASONING_MESSAGE_START') {\n return { action: 'status', status: 'thinking' };\n }\n\n if (type === 'REASONING_MESSAGE_CONTENT' || type === 'REASONING_MESSAGE_CHUNK') {\n // Reasoning text — surface it in the dedicated thinking pane\n const delta = evt.delta as string | undefined;\n if (delta) {\n return { action: 'status', status: 'thinking_text', thinkingContent: delta };\n }\n return { action: 'status', status: 'thinking' };\n }\n\n if (type === 'REASONING_MESSAGE_END' || type === 'REASONING_END' || type === 'REASONING_ENCRYPTED_VALUE') {\n return { action: 'noop' };\n }\n\n // --- State management ---\n\n if (type === 'STATE_SNAPSHOT' || type === 'STATE_DELTA' || type === 'MESSAGES_SNAPSHOT') {\n // State sync — not mapped to chat UI currently\n return { action: 'noop' };\n }\n\n // --- Activity events ---\n\n if (type === 'ACTIVITY_SNAPSHOT' || type === 'ACTIVITY_DELTA') {\n return { action: 'noop' };\n }\n\n // --- Pass-through / custom ---\n\n if (type === 'RAW' || type === 'CUSTOM') {\n return { action: 'noop' };\n }\n\n return { action: 'noop' };\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type {\n AgentStatusState,\n ApiEndpoints,\n BackendType,\n ChatContextUsage,\n ChatFile,\n ChatMessage,\n ToolApprovalDecision,\n ToolApprovalProposal,\n} from '../types';\nimport type { ParsedAction, ProtocolContext } from './protocols';\nimport { parseAgUiEvent, parseLegacyEvent, parseRestEvent } from './protocols';\nimport { parseToolApprovalProposals } from './protocols/parseRestEvent';\n\nconst STORAGE_KEY = 'filigranChatConversationId';\nconst LEGACY_CHAT_ID_KEY = 'filigranChatLegacyChatId';\n\n/**\n * Unsent composer text is kept per conversation so closing the panel (hosts\n * unmount `<ChatPanel/>` when it is closed) or switching conversation doesn't\n * discard a half-written question. `sessionStorage`, not `localStorage`: a\n * draft is scoped to the tab and dies with it.\n */\nconst DRAFT_KEY_PREFIX = 'filigranChatDraft:';\n/** Debounce on draft writes so a fast typist doesn't hit storage per keystroke. */\nconst DRAFT_PERSIST_DELAY_MS = 300;\n\nconst draftKey = (conversationId: string | null): string => `${DRAFT_KEY_PREFIX}${conversationId ?? 'new'}`;\n\nfunction loadDraft(conversationId: string | null): string {\n if (typeof window === 'undefined') return '';\n try {\n return sessionStorage.getItem(draftKey(conversationId)) ?? '';\n } catch {\n // Storage can throw in restricted/private browsing contexts — a chat\n // without draft recovery is far better than a chat that fails to mount.\n return '';\n }\n}\n\nfunction persistDraft(conversationId: string | null, value: string): void {\n if (typeof window === 'undefined') return;\n try {\n if (value) sessionStorage.setItem(draftKey(conversationId), value);\n else sessionStorage.removeItem(draftKey(conversationId));\n } catch {\n /* ignore — see loadDraft */\n }\n}\n\n/**\n * How often to check on a turn resumed by a decision made on a recovered\n * (streamless) prompt, and the backstop that ends the watch regardless.\n *\n * Such a turn has no stream left to write to — the backend persists the answer\n * and suppresses the live `done` frame — so the panel watches the pause-recovery\n * route instead, which reports whether the turn is still running. That is a\n * definite stop condition rather than a guess at how long an agent might take,\n * and each poll doubles as the sign of life that keeps the turn from being\n * abandoned. The deadline is only a backstop against a turn-state marker that\n * somehow never clears; the ordinary end is the turn reporting itself idle.\n */\nconst RESUME_POLL_MS = 5000;\nconst RESUME_WATCH_MS = 900000;\n\n/**\n * How often to re-assert that somebody is still here while a recovered prompt\n * is on screen.\n *\n * A paused turn gives up after 30 minutes with no sign of a client, and after a\n * reload the stream that would have vouched for the reviewer is gone for good —\n * so presence is inferred from requests about the conversation instead. A tab\n * left open on the prompt IS a client present, but it makes no requests, so\n * without this the turn would abandon a reviewer who is simply taking their\n * time. Well inside the server's window, since missing it costs the whole turn.\n */\nconst APPROVAL_PRESENCE_INTERVAL_MS = 600000;\n\n/** Maximum number of files that can be attached to a single message. */\nconst DEFAULT_MAX_FILE_COUNT = 10;\n/** Maximum total size of all attached files (50 MB). */\nconst DEFAULT_MAX_TOTAL_SIZE = 50 * 1024 * 1024;\n\ninterface UseChatOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n agentSlug: string | null | undefined;\n requestHeaders?: Record<string, string>;\n /** Arbitrary host page/application context, sent as `context` on the REST message body. */\n pageContext?: Record<string, unknown>;\n t: (key: string) => string;\n maxFileCount?: number;\n maxTotalSize?: number;\n}\n\nexport interface TransferredAgent {\n id: string;\n name: string;\n}\n\ninterface UseChatReturn {\n messages: ChatMessage[];\n inputValue: string;\n setInputValue: (value: string) => void;\n isLoading: boolean;\n agentStatus: AgentStatusState | null;\n attachedFiles: ChatFile[];\n conversationId: string | null;\n /**\n * Context-window occupancy for the active conversation, or `null` while the\n * backend has reported none (a fresh chat, or a backend that does not carry\n * the figures at all). Tracked live off the per-iteration progress frames and\n * finalised on `done`.\n */\n contextUsage: ChatContextUsage | null;\n transferredAgent: TransferredAgent | null;\n /**\n * True while a response is streaming AND the typed text can be dispatched\n * immediately as a mid-run steering message (REST backend with a steer\n * endpoint and a known conversation id). Gates the steering affordances in\n * the composer (accent Send next to Stop, \"Enter to send now\" copy).\n */\n canSteer: boolean;\n /**\n * Tool calls the running turn has stopped on, or `null` when nothing is\n * waiting. While set, the turn is paused mid-answer: the stream is open and\n * silent, and only a decision (or aborting the turn) moves it on.\n */\n pendingApprovals: ToolApprovalProposal[] | null;\n /** True while a decision set is in flight to the approve endpoint. */\n isSubmittingApproval: boolean;\n /**\n * Why the last decision submission failed, or `null`. Kept alongside the\n * still-visible prompt rather than replacing it: the turn is paused either\n * way, so the reviewer needs the retry as much as the explanation.\n */\n approvalError: string | null;\n /**\n * Answer a paused turn. Every proposed call must appear exactly once — the\n * backend refuses a partial set, because resuming with an undecided call\n * leaves a `tool_use` block without its `tool_result`, which the model\n * providers reject outright.\n */\n submitApprovalDecisions: (decisions: ToolApprovalDecision[]) => Promise<void>;\n /**\n * True while waiting for the answer to a decision made on a recovered prompt.\n * The turn resumed without a stream to report on, so the panel shows the\n * working indicator itself and re-reads the conversation until the answer\n * lands.\n */\n isResumingAfterDecision: boolean;\n /**\n * Bumped whenever the conversation must be re-read from the server. The panel\n * owns the restore, so this is how the hook asks for one.\n */\n historyReloadNonce: number;\n historyLoadedRef: React.MutableRefObject<boolean>;\n /**\n * Ref mirror of {@link conversationId}, always current across async\n * boundaries. Exposed so the history-restore effect can tell, when its\n * `/chat/sessions` response arrives, whether the conversation it was issued\n * for is still the active one — and ignore a genuinely superseded response\n * (new chat / agent switch) without discarding a restore that was merely\n * torn down by a benign host re-render or a StrictMode double-invoke.\n */\n conversationIdRef: React.MutableRefObject<string | null>;\n handleFileAdd: (fileList: FileList | null) => void;\n handlePaste: (e: React.ClipboardEvent) => void;\n handleSendMessage: () => Promise<void>;\n handleNewChat: () => void;\n handleStopGenerating: () => void;\n setAttachedFiles: React.Dispatch<React.SetStateAction<ChatFile[]>>;\n setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>;\n /**\n * Seed the context gauge from outside a live turn — the history-restore path,\n * which reads the newest restored assistant message's figures.\n */\n setContextUsage: React.Dispatch<React.SetStateAction<ChatContextUsage | null>>;\n /**\n * Set (or clear) the active conversation id, keeping React state, the\n * cross-async-boundary ref mirror, and localStorage all in sync. Pass\n * `null` to reset. Prefer this over a raw state setter so the id consumed\n * by `handleSendMessage` (which reads the ref) never drifts from what the\n * UI shows.\n */\n updateConversationId: (id: string | null) => void;\n /**\n * Switch to another existing conversation (history menu). Aborts any\n * in-flight request, clears the transcript, and re-arms the history-restore\n * effect so the selected conversation's messages are fetched via the\n * sessions endpoint.\n */\n handleSwitchConversation: (id: string) => void;\n}\n\nfunction getParser(backendType: BackendType): (evt: Record<string, unknown>, ctx: ProtocolContext) => ParsedAction {\n switch (backendType) {\n case 'legacy':\n return parseLegacyEvent;\n case 'ag-ui':\n return parseAgUiEvent;\n default:\n return parseRestEvent;\n }\n}\n\nfunction buildRequestBody(\n backendType: BackendType,\n content: string,\n opts: {\n legacyChatId: string | null;\n conversationId: string | null;\n agentSlug: string | null | undefined;\n pageContext?: Record<string, unknown>;\n /** See {@link ApiEndpoints.approve} — derived, never a prop of its own. */\n supportsToolApproval?: boolean;\n },\n): Record<string, unknown> {\n switch (backendType) {\n case 'legacy':\n return { question: content, chatId: opts.legacyChatId ?? undefined, streaming: true };\n case 'ag-ui':\n return {\n threadId: opts.conversationId ?? crypto.randomUUID(),\n runId: crypto.randomUUID(),\n messages: [{ id: crypto.randomUUID(), role: 'user', content }],\n tools: [],\n context: [],\n state: {},\n forwardedProps: opts.agentSlug ? { agentSlug: opts.agentSlug } : {},\n };\n default: {\n const body: Record<string, unknown> = { content, conversation_id: opts.conversationId, agent_slug: opts.agentSlug };\n // Tell the backend this turn may pause on a gated tool call and that we\n // will answer. Sent only when the host named an approve path, because\n // the flag is a promise: a backend that pauses waits indefinitely, so a\n // client that cannot answer must never claim it can. Omitted rather than\n // sent false, so a proxy that rebuilds the body from a fixed field list\n // drops nothing meaningful.\n if (opts.supportsToolApproval) {\n body.supports_tool_approval = true;\n }\n // Forward arbitrary host page context (e.g. current URL) so the agent\n // knows where the user is. Omitted when empty to keep payloads lean.\n // Guard serialization: the whole body is later JSON.stringify'd, so a\n // non-serializable value (circular reference, BigInt, …) would otherwise\n // throw and break the message send. Drop the context instead — page\n // context is supplementary and must never prevent a message from going out.\n // Decide using the serialized result so values that normalize to an empty\n // object (e.g. `{ url: undefined }`, `{ fn: () => {} }`) are also omitted.\n if (opts.pageContext && Object.keys(opts.pageContext).length > 0) {\n try {\n const serialized = JSON.stringify(opts.pageContext);\n if (serialized && serialized !== '{}') {\n body.context = opts.pageContext;\n }\n } catch {\n // Non-serializable page context — skip it rather than fail the send.\n }\n }\n return body;\n }\n }\n}\n\nexport function useChat({\n apiBaseUrl,\n apiEndpoints,\n backendType = 'rest',\n agentSlug,\n requestHeaders,\n pageContext,\n t,\n maxFileCount = DEFAULT_MAX_FILE_COUNT,\n maxTotalSize = DEFAULT_MAX_TOTAL_SIZE,\n}: UseChatOptions): UseChatReturn {\n const isLegacy = backendType === 'legacy';\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [agentStatus, setAgentStatus] = useState<AgentStatusState | null>(null);\n const [conversationId, setConversationId] = useState<string | null>(() => {\n if (typeof window === 'undefined') return null;\n return localStorage.getItem(STORAGE_KEY);\n });\n // Seeded from the persisted draft of whichever conversation we mount into,\n // so re-opening the panel restores what the user had typed.\n const [inputValue, setInputValue] = useState(() => loadDraft(typeof window === 'undefined' ? null : localStorage.getItem(STORAGE_KEY)));\n const [attachedFiles, setAttachedFiles] = useState<ChatFile[]>([]);\n const [transferredAgent, setTransferredAgent] = useState<TransferredAgent | null>(null);\n // How full the model's context window is for this conversation. Conversation\n // state rather than per-message: it describes what the NEXT turn will carry,\n // which is the only thing the user can still act on.\n const [contextUsage, setContextUsage] = useState<ChatContextUsage | null>(null);\n // Tool calls the running turn is paused on. Conversation-level rather than\n // per-message: the pause belongs to the turn, not to any one bubble, and the\n // prompt outlives the segment that was streaming when it arrived.\n const [pendingApprovals, setPendingApprovals] = useState<ToolApprovalProposal[] | null>(null);\n const [isSubmittingApproval, setIsSubmittingApproval] = useState(false);\n const [approvalError, setApprovalError] = useState<string | null>(null);\n // A prompt recovered after a reload is answered the same way, but resumes\n // differently: there is no stream left to carry the rest of the turn.\n const [isResumingAfterDecision, setIsResumingAfterDecision] = useState(false);\n const [historyReloadNonce, setHistoryReloadNonce] = useState(0);\n // Drives the resume watch's next poll: each tick re-arms the effect.\n const [resumeTick, setResumeTick] = useState(0);\n const [legacyChatId, setLegacyChatId] = useState<string | null>(() => {\n if (typeof window === 'undefined') return null;\n return localStorage.getItem(LEGACY_CHAT_ID_KEY);\n });\n\n const historyLoadedRef = useRef(false);\n const abortControllerRef = useRef<AbortController | null>(null);\n const hasUsedToolsRef = useRef(false);\n // Ref mirror of conversationId — always current across async boundaries\n const conversationIdRef = useRef(conversationId);\n // Ref mirror of isLoading, so the recovery probe can tell at apply time\n // whether a live turn has started owning the prompt since it was issued.\n const isLoadingRef = useRef(isLoading);\n isLoadingRef.current = isLoading;\n // Ref mirror of pageContext so the value sent reflects the page the user is\n // on at send time, regardless of when the send handler closure was created.\n const pageContextRef = useRef(pageContext);\n pageContextRef.current = pageContext;\n // Conversation id as the paused turn itself reported it. The decision POST\n // needs an id, and the first turn of a fresh conversation has none yet —\n // `conversationId` is normally learned from `done`, which a paused turn has\n // by definition not reached. Kept separate from `conversationIdRef` rather\n // than adopted into it: a pause should not quietly switch on the affordances\n // (steering, history) that having an id unlocks.\n const approvalConversationIdRef = useRef<string | null>(null);\n // True when the pending prompt was recovered from the server rather than read\n // off a live stream — the turn is running, but nothing is listening to it.\n const approvalDetachedRef = useRef(false);\n // Conversations already asked about a pause, so the recovery probe runs once\n // per conversation instead of on every render that settles.\n const probedConversationRef = useRef<string | null>(null);\n // When the resume watch stops regardless of what the turn reports.\n const resumeDeadlineRef = useRef(0);\n // Mutex to prevent concurrent session creation\n const creatingSessionRef = useRef<Promise<string | null> | null>(null);\n // Abort controller for in-flight file uploads (cancelled on new chat)\n const uploadAbortRef = useRef<AbortController>(new AbortController());\n\n // Guard invalid consumer values and keep deterministic limits.\n const effectiveMaxFileCount = Number.isFinite(maxFileCount) && maxFileCount > 0 ? Math.floor(maxFileCount) : DEFAULT_MAX_FILE_COUNT;\n const effectiveMaxTotalSize = Number.isFinite(maxTotalSize) && maxTotalSize > 0 ? maxTotalSize : DEFAULT_MAX_TOTAL_SIZE;\n\n // Determine message endpoint URL\n const getMessagesUrl = () => {\n if (isLegacy || apiEndpoints?.singleEndpoint) {\n return apiBaseUrl; // POST directly to base URL\n }\n return `${apiBaseUrl}${apiEndpoints?.messages ?? '/chat/messages'}`;\n };\n\n // Determine mid-run steering endpoint URL (null disables steering)\n const getSteerUrl = (): string | null => {\n if (isLegacy || backendType === 'ag-ui' || apiEndpoints?.singleEndpoint || apiEndpoints?.steer === null) {\n return null;\n }\n return `${apiBaseUrl}${apiEndpoints?.steer ?? '/chat/messages/steer'}`;\n };\n\n /**\n * Determine the tool-approval endpoint URL, or null when this host has not\n * opted in.\n *\n * Unlike its siblings there is no default path — see {@link ApiEndpoints.approve}.\n * Whether this returns a URL is exactly what decides if the widget advertises\n * `supports_tool_approval`, so \"configured\" and \"able to answer\" are the same\n * fact rather than two that can drift apart.\n */\n const getApproveUrl = (): string | null => {\n if (isLegacy || backendType === 'ag-ui' || apiEndpoints?.singleEndpoint) return null;\n const path = apiEndpoints?.approve;\n if (!path) return null;\n return `${apiBaseUrl}${path}`;\n };\n\n /**\n * Determine the pause-recovery URL for one conversation, or null when the\n * host has not exposed the route. See {@link ApiEndpoints.pendingApprovals}.\n */\n const getPendingApprovalsUrl = (convId: string): string | null => {\n if (isLegacy || backendType === 'ag-ui' || apiEndpoints?.singleEndpoint) return null;\n const base = apiEndpoints?.pendingApprovals;\n if (!base) return null;\n return `${apiBaseUrl}${base}/${convId}/pending-approvals`;\n };\n\n /**\n * Read what a conversation is paused on, and whether its turn is still going.\n *\n * One request serves three purposes: recovering a prompt the page never saw,\n * telling a resumed turn's watcher when to stop, and counting as the sign of\n * life that keeps a paused turn from being abandoned. Returns null on any\n * failure — every caller treats that as \"learned nothing\" and tries again or\n * leaves the panel as it was.\n */\n const readPendingApprovals = async (convId: string): Promise<{ proposals?: ToolApprovalProposal[]; turnRunning: boolean } | null> => {\n const url = getPendingApprovalsUrl(convId);\n if (!url) return null;\n try {\n const res = await fetch(url, { headers: { ...(requestHeaders ?? {}) } });\n if (!res.ok) return null;\n const data = await res.json();\n return {\n proposals: parseToolApprovalProposals(data?.proposals),\n // Anything but an explicit `running` ends the watch. A backend that\n // omits the field is one that cannot say, and waiting forever on a\n // turn nobody can report on is the worse failure.\n turnRunning: data?.turn === 'running',\n };\n } catch {\n return null;\n }\n };\n\n /**\n * Ask the panel to re-read the conversation from the server.\n *\n * Clearing the guard alone would not do it — the restore effect lives in the\n * panel and only re-runs when something in its dependencies changes, which is\n * what the nonce is for.\n */\n const reloadHistory = useCallback(() => {\n historyLoadedRef.current = false;\n setHistoryReloadNonce((n) => n + 1);\n }, []);\n\n // Determine upload endpoint URL (null disables file upload proxying)\n const getUploadUrl = (): string | null => {\n if (isLegacy || apiEndpoints?.singleEndpoint || apiEndpoints?.upload === null) {\n return null;\n }\n return `${apiBaseUrl}${apiEndpoints?.upload ?? '/chat/upload'}`;\n };\n\n // Determine sessions endpoint URL\n const getSessionsUrl = (): string | null => {\n if (isLegacy || apiEndpoints?.singleEndpoint || apiEndpoints?.sessions === null) {\n return null;\n }\n return `${apiBaseUrl}${apiEndpoints?.sessions ?? '/chat/sessions'}`;\n };\n\n /**\n * Update conversationId in React state, the ref mirror, and localStorage.\n * Stable identity (useCallback) so it can be used as an effect dependency.\n */\n const updateConversationId = useCallback((id: string | null) => {\n conversationIdRef.current = id;\n setConversationId(id);\n if (id) {\n localStorage.setItem(STORAGE_KEY, id);\n } else {\n localStorage.removeItem(STORAGE_KEY);\n }\n }, []);\n\n /**\n * Ensure a conversation exists. Uses a mutex so concurrent callers\n * (e.g. multiple files selected at once) share a single session creation.\n */\n const ensureConversation = async (slug: string | null | undefined): Promise<string | null> => {\n // Fast path: already have one\n if (conversationIdRef.current) return conversationIdRef.current;\n\n // If another call is already creating, wait for it\n if (creatingSessionRef.current) return creatingSessionRef.current;\n\n const sessionsUrl = getSessionsUrl();\n if (!sessionsUrl) return null;\n\n const promise = (async () => {\n try {\n const res = await fetch(sessionsUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({ agent_slug: slug }),\n });\n if (!res.ok) return null;\n const data = await res.json();\n const convId = (data?.conversation_id as string) ?? null;\n if (convId) {\n updateConversationId(convId);\n }\n return convId;\n } catch {\n return null;\n } finally {\n creatingSessionRef.current = null;\n }\n })();\n\n creatingSessionRef.current = promise;\n return promise;\n };\n\n /**\n * Upload a single file to the backend and return its file_id.\n */\n const uploadSingleFile = async (file: File, convId: string, signal: AbortSignal): Promise<string> => {\n const uploadUrl = getUploadUrl()!;\n const formData = new FormData();\n formData.append('conversation_id', convId);\n formData.append('file', file, file.name);\n\n const uploadHeaders = requestHeaders\n ? Object.fromEntries(\n Object.entries(requestHeaders).filter(([k]) => {\n const key = k.toLowerCase();\n return key !== 'content-type';\n }),\n )\n : undefined;\n\n const res = await fetch(uploadUrl, {\n method: 'POST',\n headers: uploadHeaders,\n body: formData,\n signal,\n });\n if (!res.ok) {\n throw new Error(`File upload failed: ${res.status}`);\n }\n const data = await res.json();\n const ids: string[] = data.file_ids ?? [];\n if (ids.length === 0) throw new Error('No file_id returned');\n return ids[0];\n };\n\n /**\n * Handle file selection: validate limits, add files to state immediately,\n * then upload them in the background. Each file chip shows its upload status.\n */\n const handleFileAdd = (fileList: FileList | null) => {\n if (!fileList || fileList.length === 0 || !getUploadUrl()) return;\n\n // Build the list of accepted files outside the state updater (pure logic)\n const incoming = Array.from(fileList);\n\n // We need current state to check limits — use a ref-like approach:\n // read attachedFiles via a one-shot updater that returns prev unchanged,\n // then compute outside. Simpler: just compute optimistically and let the\n // updater do the final gating.\n\n // Pre-generate stable IDs and entries so side effects use the same IDs\n const candidates: { file: File; tempId: string }[] = incoming.map((file) => ({\n file,\n tempId: crypto.randomUUID(),\n }));\n\n // Update state (pure — no side effects)\n let accepted: { file: File; tempId: string }[] = [];\n setAttachedFiles((prev) => {\n const currentCount = prev.length;\n const currentSize = prev.reduce((sum, f) => sum + f.size, 0);\n\n const slotsAvailable = effectiveMaxFileCount - currentCount;\n if (slotsAvailable <= 0) return prev;\n\n let sizeLeft = effectiveMaxTotalSize - currentSize;\n const filtered: { file: File; tempId: string }[] = [];\n for (const c of candidates.slice(0, slotsAvailable)) {\n if (c.file.size <= sizeLeft) {\n filtered.push(c);\n sizeLeft -= c.file.size;\n }\n }\n if (filtered.length === 0) return prev;\n\n accepted = filtered;\n\n const newEntries: ChatFile[] = filtered.map(({ file, tempId }) => ({\n name: file.name,\n type: file.type,\n size: file.size,\n rawFile: file,\n uploadStatus: 'pending' as const,\n fileId: tempId,\n }));\n\n return [...prev, ...newEntries];\n });\n\n // Launch uploads OUTSIDE the state updater (side effects)\n // Use setTimeout(0) to ensure state has settled after the updater\n setTimeout(() => {\n const signal = uploadAbortRef.current.signal;\n for (const { file, tempId } of accepted) {\n (async () => {\n try {\n const convId = await ensureConversation(agentSlug);\n if (!convId) {\n setAttachedFiles((p) => p.map((f) => (f.fileId === tempId ? { ...f, uploadStatus: 'error' } : f)));\n return;\n }\n const fileId = await uploadSingleFile(file, convId, signal);\n setAttachedFiles((p) => p.map((f) => (f.fileId === tempId ? { ...f, fileId, uploadStatus: 'done' } : f)));\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') return;\n setAttachedFiles((p) => p.map((f) => (f.fileId === tempId ? { ...f, uploadStatus: 'error' } : f)));\n }\n })();\n }\n }, 0);\n };\n\n const handlePaste = (e: React.ClipboardEvent) => {\n const { files } = e.clipboardData;\n if (files.length > 0) {\n e.preventDefault();\n handleFileAdd(files);\n }\n };\n\n /**\n * Mid-run steering: dispatch a message while the agent is still generating.\n * The user bubble is added optimistically and the steer endpoint is POSTed;\n * the backend persists the message and injects it into the running agentic\n * loop at the next iteration boundary. On failure (network error, or a\n * backend without steering support answering non-2xx) the optimistic bubble\n * is rolled back and the text is restored into the composer — prepended on\n * its own line if the user already typed something new — so the message is\n * never silently lost and never resets the in-flight run state.\n */\n const steerMessage = async (content: string) => {\n const steerUrl = getSteerUrl();\n const convId = conversationIdRef.current;\n if (!steerUrl || !convId) return;\n\n const optimistic: ChatMessage = {\n id: crypto.randomUUID(),\n role: 'user',\n content,\n timestamp: new Date(),\n };\n setMessages((prev) => [...prev, optimistic]);\n\n try {\n const res = await fetch(steerUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({ conversation_id: convId, content, agent_slug: agentSlug }),\n });\n if (!res.ok) throw new Error(`Steer failed: ${res.status}`);\n } catch {\n setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));\n setInputValue((prev) => (prev ? `${content}\\n${prev}` : content));\n }\n };\n\n /**\n * Answer a turn that paused on a gated tool call.\n *\n * The decision travels the same way a steering message does — a POST beside\n * the open stream — and for the same reason: the turn is still running, so\n * the answer cannot ride on a new one. The paused turn is polling for it and\n * resumes on the stream that is already open, with no lost context.\n *\n * Deliberately not carried on the turn's own AbortController: the two are\n * independent requests, and aborting the turn must not look like a decision.\n *\n * A failure leaves the prompt on screen. The alternative — clearing it —\n * would strand the turn paused with nothing to answer it, which is the exact\n * state this whole opt-in exists to avoid.\n */\n const submitApprovalDecisions = async (decisions: ToolApprovalDecision[]) => {\n const approveUrl = getApproveUrl();\n const convId = approvalConversationIdRef.current ?? conversationIdRef.current;\n // Nothing decided is a no-op, not a failure — the prompt only submits a\n // full set, so this is unreachable from the UI.\n if (decisions.length === 0) return;\n // Missing prerequisites must be VISIBLE. The prompt is on screen, the turn\n // is paused behind it, and a silent return would leave the reviewer\n // clicking a control that does nothing — the exact failure this feature\n // exists to prevent, reintroduced at the last step. Reachable if the host\n // never named an approve path, or if a paused turn arrived without a\n // conversation id on the first turn of a fresh conversation, where the id\n // is otherwise only learned from `done`.\n if (!approveUrl || !convId) {\n setApprovalError(t('This decision could not be sent. Reload the chat and try again.'));\n return;\n }\n\n setApprovalError(null);\n setIsSubmittingApproval(true);\n try {\n const res = await fetch(approveUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({\n conversation_id: convId,\n decisions: decisions.map((d) => ({\n tool_call_id: d.toolCallId,\n decision: d.verdict,\n // Only ever sent with a rejection, and only when the reviewer\n // actually wrote something — an empty string reaches the agent as\n // a reason that says nothing.\n ...(d.verdict === 'reject' && d.rejectionReason ? { rejection_reason: d.rejectionReason } : {}),\n })),\n }),\n });\n if (res.ok) {\n setPendingApprovals(null);\n if (approvalDetachedRef.current) {\n // Answered after a reload: the turn resumes, but the stream it would\n // have reported on died with the old page. The backend persists the\n // answer and suppresses the live `done`, so re-reading the\n // conversation is the only way it can ever appear — and without this\n // the click would look like it did nothing at all.\n approvalDetachedRef.current = false;\n resumeDeadlineRef.current = Date.now() + RESUME_WATCH_MS;\n setIsResumingAfterDecision(true);\n return;\n }\n // The turn is moving again; show that immediately rather than leaving\n // the composer looking idle until the next server event lands.\n setAgentStatus((prev) => (prev ? { ...prev, status: 'thinking' } : { status: 'thinking' }));\n return;\n }\n // 409: nothing is waiting any more — the turn finished, was cancelled, or\n // somebody else answered it. Distinguished from a transient failure\n // because retrying cannot help; the stream ending clears the prompt.\n setApprovalError(\n res.status === 409\n ? t('This turn is no longer waiting for a decision.')\n : t('Could not send your decision. Please try again.'),\n );\n } catch {\n setApprovalError(t('Could not send your decision. Please try again.'));\n } finally {\n setIsSubmittingApproval(false);\n }\n };\n\n const handleSendMessage = async () => {\n const steerText = inputValue.trim();\n if (isLoading) {\n // Mid-run steering — text-only sends while a response is streaming.\n // Attachments keep the legacy wait behavior (the upload + message pair\n // cannot be injected into a running loop).\n if (steerText && attachedFiles.length === 0 && getSteerUrl() && conversationIdRef.current) {\n setInputValue('');\n await steerMessage(steerText);\n }\n return;\n }\n if (!inputValue.trim() && attachedFiles.length === 0) return;\n const content = inputValue.trim();\n\n const userMsg: ChatMessage = {\n id: crypto.randomUUID(),\n role: 'user',\n content,\n timestamp: new Date(),\n files: attachedFiles.length > 0 ? [...attachedFiles] : undefined,\n };\n setMessages((prev) => [...prev, userMsg]);\n setInputValue('');\n // Clear attachment chips after sending so the input returns to a clean state.\n setAttachedFiles([]);\n setIsLoading(true);\n setAgentStatus({ status: 'thinking' });\n hasUsedToolsRef.current = false;\n\n const assistantId = crypto.randomUUID();\n setMessages((prev) => [...prev, { id: assistantId, role: 'assistant', content: '', timestamp: new Date() }]);\n\n // The assistant message currently being streamed into. A steered turn can\n // produce multiple response segments on one SSE stream: the backend\n // completes the current segment (intermediate `done`), then runs a\n // follow-up pass for the steering message (fresh `thinking` + `stream`\n // events). Each segment gets its own assistant bubble. Declared outside\n // the try so the catch below writes the error into the LIVE segment, not\n // an already-completed one.\n let currentAssistantId = assistantId;\n\n try {\n const controller = new AbortController();\n abortControllerRef.current = controller;\n\n // Collect file_ids from already-uploaded files (uploaded eagerly on selection)\n const fileIds = (userMsg.files ?? []).filter((f) => f.uploadStatus === 'done' && f.fileId).map((f) => f.fileId!);\n\n // Step 1: Send the message (with file_ids if files were uploaded)\n // Use conversationIdRef to get the latest value (may have been set by eager upload)\n const requestBody = buildRequestBody(backendType, content, {\n legacyChatId,\n conversationId: conversationIdRef.current,\n agentSlug,\n pageContext: pageContextRef.current,\n supportsToolApproval: getApproveUrl() !== null,\n });\n if (fileIds.length > 0) {\n (requestBody as Record<string, unknown>).file_ids = fileIds;\n }\n\n setAgentStatus({ status: 'thinking' });\n\n const res = await fetch(getMessagesUrl(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n });\n\n if (!res.ok || !res.body) {\n setMessages((prev) =>\n prev.map((m) => (m.id === assistantId ? { ...m, content: t('Unable to connect. Please check the configuration.') } : m)),\n );\n return;\n }\n\n const parseEvent = getParser(backendType);\n const ctx: ProtocolContext = { hasUsedTools: false, activeNodeId: '' };\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let accumulated = '';\n let doneReceived = false;\n\n /**\n * Open a new response segment when events keep flowing after a `done`.\n * Appends a fresh empty assistant message (after any steered user\n * bubble) and resets the per-segment accumulators. The fresh `thinking`\n * status also clears the reasoning window — each segment carries its\n * own reasoning, mirroring the web chat behavior.\n */\n const ensureSegment = () => {\n if (!doneReceived) return;\n doneReceived = false;\n accumulated = '';\n currentAssistantId = crypto.randomUUID();\n const segmentId = currentAssistantId;\n setMessages((prev) => [...prev, { id: segmentId, role: 'assistant', content: '', timestamp: new Date() }]);\n setAgentStatus({ status: 'thinking' });\n };\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n for (const rawLine of lines) {\n const line = rawLine.replace(/\\r$/, '');\n if (!line.startsWith('data:')) continue;\n const jsonStr = line.startsWith('data: ') ? line.slice(6) : line.slice(5);\n try {\n const evt = JSON.parse(jsonStr) as Record<string, unknown>;\n const parsed: ParsedAction = parseEvent(evt, ctx);\n\n // Sync ref → context for cross-event tracking\n ctx.hasUsedTools = ctx.hasUsedTools || hasUsedToolsRef.current;\n\n switch (parsed.action) {\n case 'status': {\n ensureSegment();\n const segId = currentAssistantId;\n // Orthogonal to the status label below: a progress frame can\n // carry a fresh context reading whatever phase it announces.\n if (parsed.contextUsage) setContextUsage(parsed.contextUsage);\n if (parsed.status === 'tool_start') hasUsedToolsRef.current = true;\n if (parsed.status === 'stream_retract') {\n // Rare: text that streamed as a provisional answer turned\n // out to precede tool calls — discard the answer bubble\n // (the text re-arrives as thinking_text right after, so it\n // lands in the reasoning window instead).\n accumulated = '';\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: '' } : m)));\n setAgentStatus((prev) => ({\n status: 'analyzing',\n thinkingContent: prev?.thinkingContent,\n }));\n } else if (parsed.status === 'thinking_text') {\n setAgentStatus((prev) => ({\n ...prev,\n status: prev?.status ?? 'thinking',\n thinkingContent: (prev?.thinkingContent ?? '') + (parsed.thinkingContent ?? ''),\n }));\n } else if (parsed.status === 'tool_heartbeat') {\n // Liveness signal during a long tool execution: update the\n // elapsed counter but KEEP the current status label/tools —\n // replacing the status would flip e.g. \"Waiting for\n // background task…\" back to \"Thinking…\" mid-execution.\n // Re-anchor the elapsed origin on every beat so the local\n // 1s ticker in ChatThinking stays true to the server clock.\n const elapsedStartMs = typeof parsed.elapsedS === 'number' ? Date.now() - parsed.elapsedS * 1000 : undefined;\n setAgentStatus((prev) =>\n prev\n ? { ...prev, elapsedS: parsed.elapsedS, elapsedStartMs }\n : { status: 'tool_start', tools: parsed.tools, elapsedS: parsed.elapsedS, elapsedStartMs },\n );\n } else {\n setAgentStatus((prev) => ({\n status: parsed.status,\n tools: parsed.tools,\n thinkingContent: prev?.thinkingContent,\n }));\n }\n break;\n }\n\n case 'stream': {\n ensureSegment();\n accumulated += parsed.content;\n // Snapshot the segment id and text: the state updater runs\n // asynchronously and `currentAssistantId` / `accumulated` may\n // already belong to the NEXT segment by then.\n const segId = currentAssistantId;\n const text = accumulated;\n setAgentStatus((prev) => ({ status: 'streaming', thinkingContent: prev?.thinkingContent }));\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: text } : m)));\n break;\n }\n\n case 'done': {\n doneReceived = true;\n // A completed segment cannot still be waiting on a decision, so\n // any prompt left standing (a submission that failed against a\n // turn somebody else already answered) is stale.\n setPendingApprovals(null);\n setApprovalError(null);\n if (parsed.conversationId) {\n updateConversationId(parsed.conversationId);\n }\n // Closing reading wins: a turn whose last iteration compacted\n // ends lower than it peaked mid-run.\n if (parsed.contextUsage) setContextUsage(parsed.contextUsage);\n if (parsed.transferAgentId && parsed.transferAgentName) {\n setTransferredAgent({ id: parsed.transferAgentId, name: parsed.transferAgentName });\n }\n const segId = currentAssistantId;\n const finalContent = parsed.content || accumulated;\n setMessages((prev) =>\n prev.map((m) =>\n m.id === segId\n ? {\n ...m,\n content: finalContent,\n toolNames: parsed.toolNames,\n toolCallCount: parsed.toolCallCount,\n iterations: parsed.iterations,\n attachments: parsed.attachments,\n reasoning: parsed.reasoning,\n toolCallTrace: parsed.toolCallTrace,\n transferChain: parsed.transferChain,\n isTruncated: parsed.isTruncated,\n }\n : m,\n ),\n );\n break;\n }\n\n case 'approval_required': {\n // The turn stops here and the stream goes silent until a\n // decision is POSTed back. Not an end state: no segment is\n // opened or closed, `isLoading` stays true, and the rest of\n // the turn arrives on this same reader afterwards.\n approvalConversationIdRef.current = parsed.conversationId ?? conversationIdRef.current;\n setApprovalError(null);\n setPendingApprovals(parsed.proposals);\n // Keep whatever reasoning the turn had already streamed: the\n // pause interrupts the turn, it does not start a new one, and\n // the prose leading up to the proposed call is exactly the\n // context the reviewer is about to judge it on.\n setAgentStatus((prev) => (prev ? { ...prev, status: 'awaiting_approval' } : { status: 'awaiting_approval' }));\n break;\n }\n\n case 'error': {\n ensureSegment();\n const segId = currentAssistantId;\n setMessages((prev) =>\n prev.map((m) =>\n m.id === segId ? { ...m, content: parsed.content || t('Unable to connect. Please check the configuration.') } : m,\n ),\n );\n return;\n }\n\n case 'set_chat_id':\n setLegacyChatId(parsed.chatId);\n localStorage.setItem(LEGACY_CHAT_ID_KEY, parsed.chatId);\n break;\n\n case 'noop':\n break;\n }\n\n // Keep ref in sync with context\n hasUsedToolsRef.current = ctx.hasUsedTools;\n } catch {\n /* skip malformed SSE */\n }\n }\n }\n if (accumulated && !doneReceived) {\n const segId = currentAssistantId;\n const text = accumulated;\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: text || 'No response.' } : m)));\n }\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') return;\n const segId = currentAssistantId;\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: t('Sorry, an error occurred. Please try again.') } : m)));\n } finally {\n abortControllerRef.current = null;\n setIsLoading(false);\n setAgentStatus(null);\n hasUsedToolsRef.current = false;\n // The stream is gone, so nothing can carry a decision any more —\n // whether the turn finished, errored, or was stopped. Leaving the prompt\n // up would offer an answer to a question nobody is listening for.\n setPendingApprovals(null);\n setApprovalError(null);\n approvalConversationIdRef.current = null;\n approvalDetachedRef.current = false;\n // We watched this turn end, so there is nothing for the recovery probe to\n // find — including on a fresh conversation, whose id only just arrived.\n probedConversationRef.current = conversationIdRef.current;\n }\n };\n\n const handleNewChat = () => {\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n // Cancel any in-flight file uploads\n uploadAbortRef.current.abort();\n uploadAbortRef.current = new AbortController();\n creatingSessionRef.current = null;\n setMessages([]);\n setInputValue('');\n setAttachedFiles([]);\n setIsLoading(false);\n setAgentStatus(null);\n setTransferredAgent(null);\n // A fresh (or newly selected) conversation starts with no known occupancy;\n // the restore or the first turn fills it back in. Carrying the previous\n // conversation's reading over would be a plain lie.\n setContextUsage(null);\n setPendingApprovals(null);\n setApprovalError(null);\n setIsResumingAfterDecision(false);\n approvalConversationIdRef.current = null;\n approvalDetachedRef.current = false;\n // Re-arm the probe: the conversation being switched to may well be paused.\n probedConversationRef.current = null;\n hasUsedToolsRef.current = false;\n historyLoadedRef.current = false;\n if (isLegacy) {\n setLegacyChatId(null);\n localStorage.removeItem(LEGACY_CHAT_ID_KEY);\n } else {\n updateConversationId(null);\n }\n };\n\n const handleSwitchConversation = (id: string) => {\n if (!isLegacy && id === conversationIdRef.current) return;\n // Reuse the full new-chat reset (abort in-flight request + uploads,\n // clear transcript/composer/status), then adopt the selected id and\n // re-arm the history-restore effect so the host panel fetches the\n // conversation's messages via the sessions endpoint.\n handleNewChat();\n if (!isLegacy) {\n updateConversationId(id);\n // `handleNewChat` blanked the composer; bring back this conversation's\n // own unsent draft, if any.\n setInputValue(loadDraft(id));\n }\n };\n\n const handleStopGenerating = () => {\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n setIsLoading(false);\n setAgentStatus(null);\n hasUsedToolsRef.current = false;\n // Stopping IS the way out of a pause the reviewer does not want to answer:\n // the backend waits indefinitely by design, so abandoning the stream is\n // the client's only other move.\n setPendingApprovals(null);\n setApprovalError(null);\n setIsResumingAfterDecision(false);\n approvalConversationIdRef.current = null;\n approvalDetachedRef.current = false;\n setMessages((prev) => prev.filter((m) => !(m.role === 'assistant' && !m.content)));\n };\n\n // Persist the composer draft against the conversation it belongs to. Writing\n // an empty value removes the entry, so sending (which blanks the composer)\n // also clears the draft — no explicit cleanup needed at the send sites.\n useEffect(() => {\n const id = window.setTimeout(() => persistDraft(conversationId, inputValue), DRAFT_PERSIST_DELAY_MS);\n return () => window.clearTimeout(id);\n }, [inputValue, conversationId]);\n\n // Flush the pending draft on unmount. Hosts unmount the panel the instant it\n // is closed, which would otherwise drop anything typed inside the debounce\n // window — exactly the keystrokes the draft exists to protect.\n const draftFlushRef = useRef({ conversationId, inputValue });\n draftFlushRef.current = { conversationId, inputValue };\n useEffect(\n () => () => {\n const { conversationId: id, inputValue: value } = draftFlushRef.current;\n persistDraft(id, value);\n },\n [],\n );\n\n /**\n * Recover a prompt the page never saw, or lost to a reload.\n *\n * `approval_required` is one event on one stream: reload while it is showing\n * and the panel holds nothing — not even the `tool_call_id`s a decision has\n * to name — while the turn goes on waiting for an answer that can no longer\n * be given. Asked once per conversation; an empty list is the ordinary\n * answer, and the whole probe is best-effort, since a host that has not\n * exposed the route simply keeps the pre-recovery behaviour.\n *\n * Skipped while a turn is live: that prompt arrives on the stream, and the\n * stream is the more current of the two.\n */\n useEffect(() => {\n const convId = conversationId;\n if (!convId || isLoading) return;\n if (probedConversationRef.current === convId) return;\n // Recovering a prompt there is no way to answer would only strand the\n // reviewer differently, so both routes have to be configured.\n const url = getPendingApprovalsUrl(convId);\n if (!url || !getApproveUrl()) return;\n probedConversationRef.current = convId;\n\n (async () => {\n // Best-effort: a failure leaves the panel exactly as it was, which is the\n // pre-recovery behaviour.\n const state = await readPendingApprovals(convId);\n if (!state?.proposals) return;\n // Validate at apply time against the live refs rather than dropping the\n // response on effect teardown — the same reasoning as the panel's\n // history restore. A StrictMode double-invoke or a host re-render tears\n // this effect down while the request is in flight without the\n // conversation having changed, and a teardown flag would discard the\n // recovery in exactly the case it is needed. Only a genuinely superseded\n // response is dropped: another conversation, or a live turn that has\n // since started and will carry its own prompt.\n if (conversationIdRef.current !== convId || isLoadingRef.current) return;\n approvalConversationIdRef.current = convId;\n approvalDetachedRef.current = true;\n setApprovalError(null);\n setPendingApprovals(state.proposals);\n })();\n // Keyed on the conversation and whether a turn is live; the endpoint\n // getters read current props on each run and must not re-trigger a probe.\n }, [conversationId, isLoading]);\n\n /**\n * Keep a recovered prompt answerable for as long as it is displayed.\n *\n * Only for prompts recovered after a reload: a live one is held open by its\n * own stream, which vouches for the reviewer on its own. Re-reading the\n * pending approvals is the lightest request that counts as a sign of life,\n * and its result is deliberately ignored — this is a heartbeat, not a poll,\n * and a prompt that has since gone stale is better answered with the 409 the\n * reviewer can see than made to vanish under them.\n */\n useEffect(() => {\n if (!pendingApprovals?.length || !approvalDetachedRef.current) return;\n const convId = approvalConversationIdRef.current;\n const url = convId ? getPendingApprovalsUrl(convId) : null;\n if (!url) return;\n const id = window.setInterval(() => {\n fetch(url, { headers: { ...(requestHeaders ?? {}) } }).catch(() => {\n /* A missed heartbeat is survivable: the next one is well inside the\n server's window. */\n });\n }, APPROVAL_PRESENCE_INTERVAL_MS);\n return () => window.clearInterval(id);\n // Re-armed whenever the prompt itself changes; the endpoint getter reads\n // current props on each run.\n }, [pendingApprovals]);\n\n /**\n * Watch a turn resumed by a decision made on a recovered prompt.\n *\n * The turn has no stream left to announce itself on, so its state is read\n * from the pause-recovery route: keep watching while it reports `running`,\n * and re-read the conversation once — the answer is there — when it reports\n * idle. Polling that route rather than the transcript is what makes the stop\n * condition definite instead of a guess, and it keeps the turn alive as a\n * side effect.\n *\n * A resumed turn can also pause *again* on a second gated call. With no\n * stream, this poll is the only way that prompt could ever reach the user, so\n * finding proposals puts the panel straight back into deciding.\n */\n useEffect(() => {\n if (!isResumingAfterDecision) return;\n const convId = approvalConversationIdRef.current;\n if (!convId) {\n setIsResumingAfterDecision(false);\n return;\n }\n if (Date.now() >= resumeDeadlineRef.current) {\n // Backstop only, for a turn-state marker that never clears. Re-read once\n // on the way out so a finished answer still lands.\n setIsResumingAfterDecision(false);\n reloadHistory();\n return;\n }\n\n let cancelled = false;\n const id = window.setTimeout(async () => {\n const state = await readPendingApprovals(convId);\n if (cancelled) return;\n if (!state) {\n // Learned nothing — a transient failure. Try again rather than\n // declaring a running turn finished.\n setResumeTick((n) => n + 1);\n return;\n }\n if (state.proposals) {\n approvalDetachedRef.current = true;\n setApprovalError(null);\n setPendingApprovals(state.proposals);\n setIsResumingAfterDecision(false);\n return;\n }\n if (!state.turnRunning) {\n setIsResumingAfterDecision(false);\n reloadHistory();\n return;\n }\n setResumeTick((n) => n + 1);\n }, RESUME_POLL_MS);\n\n return () => {\n cancelled = true;\n window.clearTimeout(id);\n };\n }, [isResumingAfterDecision, resumeTick, reloadHistory]);\n\n // Steering affordances are only advertised when the typed text can actually\n // be dispatched mid-run: a response is streaming (isLoading), the REST steer\n // endpoint is configured, and the conversation already has a server id (the\n // very first turn of a fresh conversation only receives its id on `done`).\n const canSteer = isLoading && getSteerUrl() !== null && conversationId !== null;\n\n return {\n messages,\n inputValue,\n setInputValue,\n isLoading,\n agentStatus,\n attachedFiles,\n conversationId,\n contextUsage,\n transferredAgent,\n canSteer,\n pendingApprovals,\n isSubmittingApproval,\n approvalError,\n submitApprovalDecisions,\n isResumingAfterDecision,\n historyReloadNonce,\n historyLoadedRef,\n conversationIdRef,\n handleFileAdd,\n handlePaste,\n handleSendMessage,\n handleNewChat,\n handleStopGenerating,\n setAttachedFiles,\n setMessages,\n setContextUsage,\n updateConversationId,\n handleSwitchConversation,\n };\n}\n","import { useEffect, useState } from 'react';\nimport type { ApiEndpoints, BackendType, XtmAgent } from '../types';\n\nconst STORAGE_AGENT_KEY = 'filigranChatAgentSlug';\n\ninterface UseAgentsOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n requestHeaders?: Record<string, string>;\n}\n\ninterface UseAgentsReturn {\n agents: XtmAgent[];\n /** True while the first fetch is in flight. */\n agentsLoading: boolean;\n /**\n * Set when the agent list could not be retrieved — the backend is\n * unreachable, or answered an error. Distinguishes \"still loading\" from\n * \"there is nothing to load\", which an empty array alone cannot.\n */\n agentsError: boolean;\n selectedAgent: XtmAgent | null;\n setSelectedAgent: React.Dispatch<React.SetStateAction<XtmAgent | null>>;\n agentMenuOpen: boolean;\n setAgentMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;\n handleSwitchAgent: (agent: XtmAgent, onSwitch?: () => void) => void;\n}\n\n/**\n * Normalize the raw agents list response.\n * Defensive: the endpoint may answer with something other than a JSON array\n * (an error envelope, an HTML error page that happens to parse), and an\n * `agents.map is not a function` thrown in ChatHeader blanks the whole panel.\n * Accepts both the bare array and the `{ agents: [...] }` envelope, mirroring\n * what useConversations tolerates, and drops entries without an agent id.\n */\nfunction parseAgents(data: unknown): XtmAgent[] {\n const rawList = Array.isArray(data)\n ? data\n : Array.isArray((data as Record<string, unknown>)?.agents)\n ? ((data as Record<string, unknown>).agents as unknown[])\n : [];\n return rawList.filter((raw): raw is XtmAgent => !!raw && typeof raw === 'object' && typeof (raw as XtmAgent).id === 'string');\n}\n\nexport function useAgents({ apiBaseUrl, apiEndpoints, backendType = 'rest', requestHeaders }: UseAgentsOptions): UseAgentsReturn {\n const [agents, setAgents] = useState<XtmAgent[]>([]);\n const [agentsLoading, setAgentsLoading] = useState(false);\n const [agentsError, setAgentsError] = useState(false);\n const [selectedAgent, setSelectedAgent] = useState<XtmAgent | null>(null);\n const [agentMenuOpen, setAgentMenuOpen] = useState(false);\n\n useEffect(() => {\n // Skip agents fetch if disabled, using single endpoint mode, or legacy backend\n if (apiEndpoints?.agents === null || apiEndpoints?.singleEndpoint || backendType === 'legacy') {\n return;\n }\n const agentsUrl = `${apiBaseUrl}${apiEndpoints?.agents ?? '/chat/agents'}`;\n setAgentsLoading(true);\n setAgentsError(false);\n fetch(agentsUrl, { headers: requestHeaders })\n .then((res) => {\n // A non-2xx is a failure, not an empty catalogue. Mapping it to `[]`\n // was why an unreachable backend left the menu spinning forever with\n // nothing to explain it.\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n return res.json();\n })\n .then((data: unknown) => {\n const list = parseAgents(data);\n setAgents(list);\n if (list.length > 0 && !selectedAgent) {\n const savedSlug = localStorage.getItem(STORAGE_AGENT_KEY);\n const match = savedSlug ? list.find((a) => a.slug === savedSlug) : null;\n setSelectedAgent(match || list[0]);\n }\n })\n .catch(() => {\n setAgents([]);\n setAgentsError(true);\n })\n .finally(() => setAgentsLoading(false));\n }, [apiBaseUrl, apiEndpoints, backendType, requestHeaders]);\n\n const handleSwitchAgent = (agent: XtmAgent, onSwitch?: () => void) => {\n if (agent.id === selectedAgent?.id) {\n setAgentMenuOpen(false);\n return;\n }\n setSelectedAgent(agent);\n if (agent.slug) localStorage.setItem(STORAGE_AGENT_KEY, agent.slug);\n setAgentMenuOpen(false);\n onSwitch?.();\n };\n\n return {\n agents,\n agentsLoading,\n agentsError,\n selectedAgent,\n setSelectedAgent,\n agentMenuOpen,\n setAgentMenuOpen,\n handleSwitchAgent,\n };\n}\n","import { useCallback, useState } from 'react';\nimport type { ApiEndpoints, BackendType, ChatConversationSummary } from '../types';\n\ninterface UseConversationsOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n requestHeaders?: Record<string, string>;\n}\n\ninterface UseConversationsReturn {\n /** Whether the history feature is available at all (REST backend with a sessions endpoint). */\n historyEnabled: boolean;\n conversations: ChatConversationSummary[];\n conversationsLoading: boolean;\n /** Fetch (or re-fetch) the conversation list. No-ops when history is disabled. */\n refreshConversations: () => Promise<void>;\n /** Delete a conversation server-side. Returns true on success. */\n deleteConversation: (id: string) => Promise<boolean>;\n /** Rename a conversation server-side. Returns true on success. */\n renameConversation: (id: string, title: string) => Promise<boolean>;\n}\n\n/**\n * Normalize one raw conversation entry from the backend list response.\n * Defensive: skips entries without a conversation id, accepts both the\n * snake_case REST shape and a few aliases so older proxies keep working.\n */\nfunction parseConversation(raw: unknown): ChatConversationSummary | null {\n if (!raw || typeof raw !== 'object') return null;\n const c = raw as Record<string, unknown>;\n const id = c.conversation_id ?? c.id;\n if (typeof id !== 'string' || !id) return null;\n // Keep the raw (trimmed) title; the localized \"Untitled conversation\"\n // fallback is applied at render time (ChatHeader) so it goes through the\n // component's translation function instead of being hardcoded in English.\n const title = typeof c.title === 'string' ? c.title.trim() : '';\n const updatedAt = typeof c.updated_at === 'string' ? c.updated_at : typeof c.created_at === 'string' ? c.created_at : undefined;\n const messageCount = typeof c.message_count === 'number' ? c.message_count : undefined;\n const agentName = typeof c.agent_name === 'string' && c.agent_name ? c.agent_name : undefined;\n return { conversationId: id, title, updatedAt, messageCount, agentName };\n}\n\n/**\n * Multi-conversation history for the REST backend (mirrors the XTM One web\n * chat sidebar). The conversation list is fetched lazily — when the history\n * menu opens — via `GET {apiBaseUrl}{sessions}`, and conversations are\n * deleted via `DELETE {apiBaseUrl}{sessions}/{conversation_id}`.\n *\n * Degrades gracefully: a backend that doesn't implement the list endpoint\n * yet (404/405/network error) simply yields an empty list, so the menu shows\n * its empty state instead of breaking the chat.\n */\nexport function useConversations({\n apiBaseUrl,\n apiEndpoints,\n backendType = 'rest',\n requestHeaders,\n}: UseConversationsOptions): UseConversationsReturn {\n const [conversations, setConversations] = useState<ChatConversationSummary[]>([]);\n const [conversationsLoading, setConversationsLoading] = useState(false);\n\n const historyEnabled = backendType === 'rest' && !apiEndpoints?.singleEndpoint && apiEndpoints?.sessions !== null && apiEndpoints?.history !== null;\n\n const sessionsUrl = `${apiBaseUrl}${apiEndpoints?.history ?? apiEndpoints?.sessions ?? '/chat/sessions'}`;\n\n const refreshConversations = useCallback(async () => {\n if (!historyEnabled) return;\n setConversationsLoading(true);\n try {\n const res = await fetch(sessionsUrl, {\n method: 'GET',\n headers: { ...(requestHeaders ?? {}) },\n });\n if (!res.ok) {\n setConversations([]);\n return;\n }\n const data: unknown = await res.json();\n const rawList = Array.isArray(data)\n ? data\n : Array.isArray((data as Record<string, unknown>)?.conversations)\n ? ((data as Record<string, unknown>).conversations as unknown[])\n : [];\n setConversations(rawList.map(parseConversation).filter((c): c is ChatConversationSummary => c !== null));\n } catch {\n setConversations([]);\n } finally {\n setConversationsLoading(false);\n }\n }, [historyEnabled, sessionsUrl, requestHeaders]);\n\n const deleteConversation = useCallback(\n async (id: string): Promise<boolean> => {\n if (!historyEnabled) return false;\n try {\n const res = await fetch(`${sessionsUrl}/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n headers: { ...(requestHeaders ?? {}) },\n });\n if (!res.ok) return false;\n setConversations((prev) => prev.filter((c) => c.conversationId !== id));\n return true;\n } catch {\n return false;\n }\n },\n [historyEnabled, sessionsUrl, requestHeaders],\n );\n\n const renameConversation = useCallback(\n async (id: string, title: string): Promise<boolean> => {\n const trimmed = title.trim();\n // An empty title is a no-op rather than an error: the backend would name\n // the conversation from its first message anyway, and silently wiping a\n // title because the user cleared the field and hit Enter is worse.\n if (!historyEnabled || !trimmed) return false;\n // Optimistic: the row is being edited in place, so waiting for the\n // round-trip would make it flicker back to the old title first.\n const previous = conversations;\n setConversations((prev) => prev.map((c) => (c.conversationId === id ? { ...c, title: trimmed } : c)));\n try {\n const res = await fetch(`${sessionsUrl}/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({ title: trimmed }),\n });\n if (!res.ok) {\n setConversations(previous);\n return false;\n }\n return true;\n } catch {\n setConversations(previous);\n return false;\n }\n },\n [historyEnabled, sessionsUrl, requestHeaders, conversations],\n );\n\n return { historyEnabled, conversations, conversationsLoading, refreshConversations, deleteConversation, renameConversation };\n}\n","import { useEffect, useRef, useState } from 'react';\nimport type { ChatMode } from '../types';\n\nconst SIDEBAR_WIDTH = 400;\nconst SIDEBAR_WIDTH_STORAGE_KEY = 'filigranChatSidebarWidth';\nconst MAX_SIDEBAR_RATIO = 0.4;\n\ninterface UseSidebarResizeOptions {\n mode: ChatMode;\n resizable: boolean;\n onWidthChange?: (width: number) => void;\n onResizeStart?: () => void;\n onResizeEnd?: () => void;\n}\n\ninterface UseSidebarResizeReturn {\n sidebarWidth: number;\n handleResizeStart: (e: React.MouseEvent) => void;\n defaultWidth: number;\n isResizing: boolean;\n}\n\nexport function useSidebarResize({ mode, resizable, onWidthChange, onResizeStart, onResizeEnd }: UseSidebarResizeOptions): UseSidebarResizeReturn {\n const [sidebarWidth, setSidebarWidth] = useState<number>(() => {\n if (typeof window === 'undefined') return SIDEBAR_WIDTH;\n const stored = localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY);\n if (stored) {\n const parsed = parseInt(stored, 10);\n if (!Number.isNaN(parsed) && parsed >= SIDEBAR_WIDTH) return parsed;\n }\n return SIDEBAR_WIDTH;\n });\n const [isResizing, setIsResizing] = useState(false);\n\n const isResizingRef = useRef(false);\n const sidebarWidthRef = useRef(sidebarWidth);\n sidebarWidthRef.current = sidebarWidth;\n const onWidthChangeRef = useRef(onWidthChange);\n onWidthChangeRef.current = onWidthChange;\n const onResizeEndRef = useRef(onResizeEnd);\n onResizeEndRef.current = onResizeEnd;\n\n // Notify parent of sidebar width when entering sidebar mode\n useEffect(() => {\n if (mode === 'sidebar' && resizable) {\n onWidthChangeRef.current?.(sidebarWidthRef.current);\n }\n }, [mode, resizable]);\n\n // Resize event handlers\n useEffect(() => {\n if (mode !== 'sidebar' || !resizable) return undefined;\n\n const handleMouseMove = (e: MouseEvent) => {\n if (!isResizingRef.current) return;\n e.preventDefault();\n const newWidth = window.innerWidth - e.clientX;\n const maxWidth = window.innerWidth * MAX_SIDEBAR_RATIO;\n const clamped = Math.min(Math.max(newWidth, SIDEBAR_WIDTH), maxWidth);\n setSidebarWidth(clamped);\n sidebarWidthRef.current = clamped;\n onWidthChangeRef.current?.(clamped);\n };\n\n const handleMouseUp = () => {\n if (!isResizingRef.current) return;\n isResizingRef.current = false;\n setIsResizing(false);\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(sidebarWidthRef.current));\n onResizeEndRef.current?.();\n };\n\n const handleWindowResize = () => {\n const maxWidth = window.innerWidth * MAX_SIDEBAR_RATIO;\n if (sidebarWidthRef.current > maxWidth) {\n const clamped = Math.max(maxWidth, SIDEBAR_WIDTH);\n setSidebarWidth(clamped);\n sidebarWidthRef.current = clamped;\n onWidthChangeRef.current?.(clamped);\n }\n };\n\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n window.addEventListener('resize', handleWindowResize);\n\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n window.removeEventListener('resize', handleWindowResize);\n };\n }, [mode, resizable]);\n\n const handleResizeStart = (e: React.MouseEvent) => {\n e.preventDefault();\n isResizingRef.current = true;\n setIsResizing(true);\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n onResizeStart?.();\n };\n\n return {\n sidebarWidth,\n handleResizeStart,\n defaultWidth: SIDEBAR_WIDTH,\n isResizing,\n };\n}\n","import { useEffect, useRef } from 'react';\n\n/**\n * A turn must run at least this long before its completion is worth a\n * notification — instant replies never raise one.\n */\nconst MIN_NOTICE_MS = 4000;\n/** Document-title flash cadence while the user is away. */\nconst TITLE_FLASH_MS = 1200;\n\n// The document title is a single page-global resource, so the flash is shared\n// across hook instances on purpose (per-hook timers would fight over the one\n// `document.title` and clobber each other's saved title). `activeHooks` ref-\n// counts mounted, enabled instances so one panel unmounting never cancels a\n// flash another panel still owns — only the last one to leave restores it.\nlet flashTimer: number | null = null;\nlet originalTitle: string | null = null;\nlet activeHooks = 0;\n\n/** Stop flashing and restore the page title captured when the flash began. */\nfunction stopTitleFlash(): void {\n if (flashTimer !== null) {\n window.clearInterval(flashTimer);\n flashTimer = null;\n }\n if (originalTitle !== null) {\n document.title = originalTitle;\n originalTitle = null;\n }\n}\n\n/**\n * Flash the document title so a multitasking user notices the answer landed\n * even from another tab or window. The flash is stopped (and the title\n * restored) the moment the tab is visible AND focused again, by the\n * hook-scoped listeners below. Title flashing needs no permission and is the\n * reliable baseline of the completion notification.\n */\nfunction startTitleFlash(message: string): void {\n if (typeof document === 'undefined') return;\n if (originalTitle === null) originalTitle = document.title;\n if (flashTimer !== null) window.clearInterval(flashTimer);\n let showMessage = true;\n document.title = message;\n flashTimer = window.setInterval(() => {\n showMessage = !showMessage;\n document.title = showMessage ? message : (originalTitle ?? message);\n }, TITLE_FLASH_MS);\n}\n\n/**\n * Best-effort OS notification — only fired when the user has already granted\n * permission. We deliberately never call `Notification.requestPermission()`\n * unprompted: the title flash (and the host toast) cover the case where OS\n * notifications are unavailable.\n */\nfunction notifyOS(title: string, body: string): void {\n try {\n if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return;\n new Notification(title, { body, tag: 'filigran-chat-complete' });\n } catch {\n /* Notification constructor can throw on some platforms — ignore. */\n }\n}\n\ninterface UseAwayCompletionNoticeOptions {\n /** True while a response is being generated. */\n isLoading: boolean;\n /** Name of the answering agent, used in the notification body. */\n agentName: string;\n t: (key: string) => string;\n /** Master switch (default true). */\n enabled?: boolean;\n /**\n * Host hook fired when a long turn finishes and the user is not actively\n * watching the chat — either away (tab hidden / another window) or in-app\n * with the chat surface closed/hidden (`isViewingChat` reports not-viewing).\n * Lets the host raise its own in-app toast (the chatbot has no toast surface\n * of its own). Receives the translated strings.\n */\n onComplete?: (title: string, body: string) => void;\n /**\n * Returns true when the chat surface is on screen for the user (the panel is\n * open and visible) — NOT merely whether focus sits inside it. When provided,\n * the notice also fires if the chat surface is hidden/closed while the tab is\n * still focused (e.g. a docked sidebar the host has collapsed). It must NOT\n * key on focus-within: in sidebar/floating mode the user reads a streamed\n * answer while their focus stays in the host app, and pinging them for an\n * answer they can already see is exactly the noise this guards against. When\n * omitted, only the away case (tab hidden / window unfocused) triggers it.\n */\n isViewingChat?: () => boolean;\n}\n\n/**\n * Notify the user when a long-running turn finishes and they are not watching\n * the chat. State is read at completion (never latched mid-turn) so someone who\n * stepped away but returned before the turn finished is not pinged:\n * - **Away** (tab hidden or another window/app focused): document-title flash +\n * OS notification (when granted) + host toast.\n * - **Surface not visible** (window focused & tab visible, but the chat panel\n * is closed/hidden — only when `isViewingChat` reports not-viewing): host\n * toast only.\n * - **Actively watching** (tab visible, window focused, panel on screen):\n * nothing — the streamed answer is itself the feedback.\n */\nexport function useAwayCompletionNotice({\n isLoading,\n agentName,\n t,\n enabled = true,\n onComplete,\n isViewingChat,\n}: UseAwayCompletionNoticeOptions): void {\n const wasLoadingRef = useRef(false);\n const startRef = useRef(0);\n\n // Stop the flash and restore the title as soon as the user returns to a\n // visible, focused tab. Bound only while enabled and torn down on unmount (or\n // when the host disables it), so we never leak listeners or leave the title\n // flashing after the panel is gone.\n useEffect(() => {\n if (!enabled || typeof document === 'undefined') return;\n activeHooks += 1;\n const clearOnReturn = () => {\n if (!document.hidden && document.hasFocus()) stopTitleFlash();\n };\n document.addEventListener('visibilitychange', clearOnReturn);\n window.addEventListener('focus', clearOnReturn);\n return () => {\n document.removeEventListener('visibilitychange', clearOnReturn);\n window.removeEventListener('focus', clearOnReturn);\n activeHooks = Math.max(0, activeHooks - 1);\n // Only restore the title once the last consumer leaves, so unmounting one\n // panel never cancels another panel's in-flight flash.\n if (activeHooks === 0) stopTitleFlash();\n };\n }, [enabled]);\n\n useEffect(() => {\n const wasLoading = wasLoadingRef.current;\n wasLoadingRef.current = isLoading;\n\n if (isLoading && !wasLoading) {\n startRef.current = Date.now();\n return;\n }\n\n if (!isLoading && wasLoading && enabled) {\n const elapsed = Date.now() - startRef.current;\n if (elapsed < MIN_NOTICE_MS) return;\n const hidden = typeof document !== 'undefined' && document.hidden;\n const unfocused = typeof document !== 'undefined' && !document.hasFocus();\n const away = hidden || unfocused;\n const viewingChat = isViewingChat ? isViewingChat() : true;\n // Focused tab + looking at the chat → the streamed answer is the feedback.\n if (!away && viewingChat) return;\n const title = t('Response ready');\n const body = agentName ? `${agentName} ${t('has finished')}` : t('Your answer is ready');\n if (away) {\n startTitleFlash(title);\n notifyOS(title, body);\n }\n onComplete?.(title, body);\n }\n }, [isLoading, enabled, agentName, t, onComplete, isViewingChat]);\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { ApiEndpoints, BackendType, ChatPromptTemplate, ChatQuotaStatus } from '../types';\n\ninterface UseComposerExtrasOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n requestHeaders?: Record<string, string>;\n}\n\ninterface UseComposerExtrasReturn {\n /** Null while unavailable — the toolbar then omits the affordance entirely. */\n prompts: ChatPromptTemplate[] | null;\n quota: ChatQuotaStatus | null;\n /** Re-read the quota after a turn completes, so the indicator stays honest. */\n refreshQuota: () => void;\n}\n\n/** Defensive parse: an unexpected payload yields no prompts rather than a crash. */\nfunction parsePrompts(data: unknown): ChatPromptTemplate[] {\n const rawList = Array.isArray(data)\n ? data\n : Array.isArray((data as Record<string, unknown>)?.prompts)\n ? ((data as Record<string, unknown>).prompts as unknown[])\n : [];\n const out: ChatPromptTemplate[] = [];\n for (const item of rawList) {\n if (!item || typeof item !== 'object') continue;\n const p = item as Record<string, unknown>;\n const id = typeof p.id === 'string' ? p.id : '';\n const content = typeof p.content === 'string' ? p.content : '';\n // A prompt with nothing to insert is not worth listing.\n if (!id || !content) continue;\n out.push({\n id,\n title: typeof p.title === 'string' && p.title ? p.title : id,\n content,\n description: typeof p.description === 'string' ? p.description : undefined,\n });\n }\n return out;\n}\n\nfunction parseQuota(data: unknown): ChatQuotaStatus | null {\n if (!data || typeof data !== 'object') return null;\n const q = data as Record<string, unknown>;\n if (typeof q.used !== 'number') return null;\n return {\n used: q.used,\n // Explicitly nullable: absent and null both mean \"no ceiling\".\n limit: typeof q.limit === 'number' ? q.limit : null,\n period: typeof q.period === 'string' ? q.period : '',\n };\n}\n\n/**\n * Fetches the two data-driven composer toolbar items: the prompt library and\n * the quota indicator.\n *\n * Both are opt-in by configuration rather than by a mode flag — a host that\n * does not serve the route (or sets it to null) simply gets no affordance, so\n * the UI can never advertise something the backend cannot answer. Neither is\n * available on the legacy / ag-ui backends or in single-endpoint mode, which\n * have no route to carry them.\n */\nexport function useComposerExtras({\n apiBaseUrl,\n apiEndpoints,\n backendType = 'rest',\n requestHeaders,\n}: UseComposerExtrasOptions): UseComposerExtrasReturn {\n const [prompts, setPrompts] = useState<ChatPromptTemplate[] | null>(null);\n const [quota, setQuota] = useState<ChatQuotaStatus | null>(null);\n\n const restLike = backendType === 'rest' && !apiEndpoints?.singleEndpoint;\n const promptsPath = apiEndpoints?.prompts === undefined ? '/chat/prompts' : apiEndpoints.prompts;\n const quotaPath = apiEndpoints?.quota === undefined ? '/chat/quota' : apiEndpoints.quota;\n const promptsUrl = restLike && promptsPath ? `${apiBaseUrl}${promptsPath}` : null;\n const quotaUrl = restLike && quotaPath ? `${apiBaseUrl}${quotaPath}` : null;\n\n useEffect(() => {\n if (!promptsUrl) {\n setPrompts(null);\n return;\n }\n let cancelled = false;\n fetch(promptsUrl, { credentials: 'include', headers: { ...(requestHeaders ?? {}) } })\n .then((res) => (res.ok ? res.json() : null))\n .then((data) => {\n if (cancelled) return;\n // A failed or empty fetch leaves the affordance hidden rather than\n // showing an empty menu the user cannot act on.\n const parsed = data === null ? [] : parsePrompts(data);\n setPrompts(parsed.length > 0 ? parsed : null);\n })\n .catch(() => {\n if (!cancelled) setPrompts(null);\n });\n return () => {\n cancelled = true;\n };\n }, [promptsUrl, requestHeaders]);\n\n const [quotaNonce, setQuotaNonce] = useState(0);\n const refreshQuota = useCallback(() => setQuotaNonce((n) => n + 1), []);\n\n useEffect(() => {\n if (!quotaUrl) {\n setQuota(null);\n return;\n }\n let cancelled = false;\n fetch(quotaUrl, { credentials: 'include', headers: { ...(requestHeaders ?? {}) } })\n .then((res) => (res.ok ? res.json() : null))\n .then((data) => {\n if (!cancelled) setQuota(parseQuota(data));\n })\n .catch(() => {\n if (!cancelled) setQuota(null);\n });\n return () => {\n cancelled = true;\n };\n }, [quotaUrl, requestHeaders, quotaNonce]);\n\n return { prompts, quota, refreshQuota };\n}\n","import { useEffect, useState } from 'react';\nimport type { ApiEndpoints, BackendType } from '../types';\n\ninterface UseAgentSuggestionsOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n requestHeaders?: Record<string, string>;\n /** Selected agent; suggestions are re-fetched whenever it changes. */\n agentSlug: string | null | undefined;\n}\n\ninterface UseAgentSuggestionsReturn {\n /** Null when unavailable — the caller then falls back to its own list. */\n suggestions: string[] | null;\n loading: boolean;\n}\n\n/** Accepts a bare array or `{ suggestions: [...] }`, and drops non-strings. */\nfunction parseSuggestions(data: unknown): string[] {\n const rawList = Array.isArray(data)\n ? data\n : Array.isArray((data as Record<string, unknown>)?.suggestions)\n ? ((data as Record<string, unknown>).suggestions as unknown[])\n : [];\n return rawList\n .map((s) => {\n if (typeof s === 'string') return s.trim();\n // Tolerate `{ label }` / `{ prompt }` objects: an \"action suggestion\"\n // is likely to grow fields, and a richer payload should not blank the\n // welcome screen for older clients.\n if (s && typeof s === 'object') {\n const o = s as Record<string, unknown>;\n const v = o.prompt ?? o.label ?? o.text;\n if (typeof v === 'string') return v.trim();\n }\n return '';\n })\n .filter((s) => s.length > 0);\n}\n\n/**\n * Suggested opening actions for the selected agent.\n *\n * Fetched per agent so the welcome screen changes when you switch — which is\n * also the confirmation that the switch took effect. Generic today; the\n * endpoint is the seam through which they can become per-user later without\n * touching this package.\n *\n * Returns null (rather than an empty list) whenever the route is unavailable\n * or answers nothing usable, so the caller can fall back to its own\n * `promptSuggestions` instead of rendering an empty section.\n */\nexport function useAgentSuggestions({\n apiBaseUrl,\n apiEndpoints,\n backendType = 'rest',\n requestHeaders,\n agentSlug,\n}: UseAgentSuggestionsOptions): UseAgentSuggestionsReturn {\n const [suggestions, setSuggestions] = useState<string[] | null>(null);\n const [loading, setLoading] = useState(false);\n\n const restLike = backendType === 'rest' && !apiEndpoints?.singleEndpoint;\n const path = apiEndpoints?.suggestions === undefined ? '/chat/suggestions' : apiEndpoints.suggestions;\n const baseUrl = restLike && path ? `${apiBaseUrl}${path}` : null;\n\n useEffect(() => {\n if (!baseUrl) {\n setSuggestions(null);\n return;\n }\n let cancelled = false;\n setLoading(true);\n // The agent is a query parameter rather than a path segment: a backend\n // that ignores it still answers with its generic set, which is exactly the\n // \"generic today, personalised later\" progression.\n const url = agentSlug ? `${baseUrl}?agent_slug=${encodeURIComponent(agentSlug)}` : baseUrl;\n fetch(url, { credentials: 'include', headers: { ...(requestHeaders ?? {}) } })\n .then((res) => (res.ok ? res.json() : null))\n .then((data) => {\n if (cancelled) return;\n const parsed = data === null ? [] : parseSuggestions(data);\n setSuggestions(parsed.length > 0 ? parsed : null);\n })\n .catch(() => {\n if (!cancelled) setSuggestions(null);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n }, [baseUrl, agentSlug, requestHeaders]);\n\n return { suggestions, loading };\n}\n","import type { IconProps } from '../../types';\n\nexport const AlertTriangleIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" />\n <path d=\"M12 9v4\" />\n <path d=\"M12 17h.01\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ArrowRightLeftIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m16 3 4 4-4 4\" />\n <path d=\"M20 7H4\" />\n <path d=\"m8 21-4-4 4-4\" />\n <path d=\"M4 17h16\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const AttachFileIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const BotIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M12 8V4H8\" />\n <rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\" />\n <path d=\"M2 14h2\" />\n <path d=\"M20 14h2\" />\n <path d=\"M15 13v2\" />\n <path d=\"M9 13v2\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const BrainIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z\" />\n <path d=\"M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z\" />\n <path d=\"M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4\" />\n <path d=\"M17.599 6.5a3 3 0 0 0 .399-1.375\" />\n <path d=\"M6.003 5.125A3 3 0 0 0 6.401 6.5\" />\n <path d=\"M3.477 10.896a4 4 0 0 1 .585-.396\" />\n <path d=\"M19.938 10.5a4 4 0 0 1 .585.396\" />\n <path d=\"M6 18a4 4 0 0 1-1.967-.516\" />\n <path d=\"M19.967 17.484A4 4 0 0 1 18 18\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const CheckCircleIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"m9 12 2 2 4-4\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const CheckIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ChevronDownIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const CloseIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const CopyIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const DatabaseIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\" />\n <path d=\"M3 5V19A9 3 0 0 0 21 19V5\" />\n <path d=\"M3 12A9 3 0 0 0 21 12\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const DefaultLogoIcon = ({ className, size = 24 }: IconProps) => (\n <svg xmlns=\"http://www.w3.org/2000/svg\" width={size} height={size} viewBox=\"0 0 24 24\" fill=\"currentColor\" stroke=\"none\" className={className}>\n <path d=\"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const DownloadIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n <polyline points=\"7 10 12 15 17 10\" />\n <line x1=\"12\" x2=\"12\" y1=\"15\" y2=\"3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const EditIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M12 20h9\" />\n <path d=\"M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ExternalLinkIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M15 3h6v6\" />\n <path d=\"M10 14 21 3\" />\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FileIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\" />\n <path d=\"M14 2v4a2 2 0 0 0 2 2h4\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FloatingIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M11 13H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7\" />\n <rect width=\"12\" height=\"12\" x=\"10\" y=\"10\" rx=\"2\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FullscreenExitIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3\" />\n <path d=\"M21 8h-3a2 2 0 0 1-2-2V3\" />\n <path d=\"M3 16h3a2 2 0 0 0 2 2v3\" />\n <path d=\"M16 21v-3a2 2 0 0 1 2-2h3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FullscreenIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3\" />\n <path d=\"M21 8V5a2 2 0 0 0-2-2h-3\" />\n <path d=\"M3 16v3a2 2 0 0 0 2 2h3\" />\n <path d=\"M16 21h3a2 2 0 0 0 2-2v-3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const GamepadIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <line x1=\"6\" x2=\"10\" y1=\"11\" y2=\"11\" />\n <line x1=\"8\" x2=\"8\" y1=\"9\" y2=\"13\" />\n <line x1=\"15\" x2=\"15.01\" y1=\"12\" y2=\"12\" />\n <line x1=\"18\" x2=\"18.01\" y1=\"10\" y2=\"10\" />\n <path d=\"M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const GlobeIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20\" />\n <path d=\"M2 12h20\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const HistoryIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n <path d=\"M3 3v5h5\" />\n <path d=\"M12 7v5l4 2\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ImageIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\" />\n <circle cx=\"9\" cy=\"9\" r=\"2\" />\n <path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const InfoIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 16v-4\" />\n <path d=\"M12 8h.01\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const MailIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\" />\n <path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const MaximizeIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M15 3h6v6\" />\n <path d=\"M9 21H3v-6\" />\n <path d=\"M21 3l-7 7\" />\n <path d=\"M3 21l7-7\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const MicIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M12 19v3\" />\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n <rect x=\"9\" y=\"2\" width=\"6\" height=\"13\" rx=\"3\" />\n </svg>\n);\n\nexport const MicOffIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M12 19v3\" />\n <path d=\"M19 10v2a7 7 0 0 1-.11 1.23\" />\n <path d=\"M5 10v2a7 7 0 0 0 12 5\" />\n <path d=\"M15 9.34V5a3 3 0 0 0-5.68-1.33\" />\n <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12\" />\n <path d=\"m2 2 20 20\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const SearchIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"m21 21-4.3-4.3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const SendIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"m22 2-7 20-4-9-9-4Z\" />\n <path d=\"m22 2-11 11\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const SidebarIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" />\n <path d=\"M15 3v18\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const SparklesIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const StopCircleIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <rect width=\"6\" height=\"6\" x=\"9\" y=\"9\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const TerminalIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <polyline points=\"4 17 10 11 4 5\" />\n <line x1=\"12\" x2=\"20\" y1=\"19\" y2=\"19\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ThumbsDownIcon = ({ className, size = 24, filled = false }: IconProps & { filled?: boolean }) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill={filled ? 'currentColor' : 'none'}\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M17 14V2\" />\n <path d=\"M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ThumbsUpIcon = ({ className, size = 24, filled = false }: IconProps & { filled?: boolean }) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill={filled ? 'currentColor' : 'none'}\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M7 10v12\" />\n <path d=\"M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const TrashIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M3 6h18\" />\n <path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\" />\n <path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\" />\n <line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\" />\n <line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const UserPlusIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <line x1=\"19\" x2=\"19\" y1=\"8\" y2=\"14\" />\n <line x1=\"22\" x2=\"16\" y1=\"11\" y2=\"11\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const WrenchIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <path d=\"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const XCircleIcon = ({ className, size = 24 }: IconProps) => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"m15 9-6 6\" />\n <path d=\"m9 9 6 6\" />\n </svg>\n);\n","import { useCallback, useLayoutEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useClickOutside } from '../hooks/useClickOutside';\nimport { findChatbotRoot } from '../utils';\n\ninterface DropdownProps {\n open: boolean;\n onClose: () => void;\n anchorRef: React.RefObject<HTMLElement | null>;\n placement?: 'bottom-start' | 'bottom-end';\n width?: number;\n children: React.ReactNode;\n}\n\n/** Breathing room between the anchor and the panel, and from the viewport edge. */\nconst GAP = 4;\nconst EDGE_MARGIN = 8;\n\nexport const Dropdown = ({ open, onClose, anchorRef, placement = 'bottom-start', width = 280, children }: DropdownProps) => {\n const panelRef = useRef<HTMLDivElement>(null);\n const [pos, setPos] = useState({ top: 0, left: 0 });\n\n const stableOnClose = useCallback(() => onClose(), [onClose]);\n useClickOutside(panelRef, stableOnClose, open);\n\n // `useLayoutEffect`, not `useEffect`: the panel is measured after it mounts\n // and then moved, so doing it after paint would show it in the wrong place\n // for a frame.\n useLayoutEffect(() => {\n if (!open || !anchorRef.current) return;\n const rect = anchorRef.current.getBoundingClientRect();\n const panelHeight = panelRef.current?.offsetHeight ?? 0;\n\n // Flip above the anchor when there isn't room below and there is more room\n // above. The composer toolbar sits at the bottom of the panel, so its\n // menus would otherwise open straight off the bottom edge and be\n // unreachable — the anchor's own position decides, so callers don't have\n // to know where they are.\n const spaceBelow = window.innerHeight - rect.bottom - EDGE_MARGIN;\n const spaceAbove = rect.top - EDGE_MARGIN;\n const flipUp = panelHeight > 0 && spaceBelow < panelHeight && spaceAbove > spaceBelow;\n const top = flipUp ? Math.max(EDGE_MARGIN, rect.top - panelHeight - GAP) : rect.bottom + GAP;\n\n // Keep it inside the viewport horizontally too: the floating panel is\n // narrow, so a menu anchored near its right edge would otherwise hang off.\n const preferredLeft = placement === 'bottom-end' ? rect.right - width : rect.left;\n const left = Math.max(EDGE_MARGIN, Math.min(preferredLeft, window.innerWidth - width - EDGE_MARGIN));\n\n setPos({ top, left });\n }, [open, anchorRef, placement, width, children]);\n\n if (!open) return null;\n\n const portalTarget = findChatbotRoot(anchorRef.current);\n\n return createPortal(\n <div\n ref={panelRef}\n className=\"fixed z-[10000] rounded-[10px] overflow-hidden border border-gray-200 dark:border-white/10 bg-white dark:bg-[#2a2a3e] shadow-xl\"\n style={{ top: pos.top, left: pos.left, width }}\n >\n {children}\n </div>,\n portalTarget,\n );\n};\n","import { useEffect, type RefObject } from 'react';\n\nexport function useClickOutside(ref: RefObject<HTMLElement | null>, handler: () => void, active = true) {\n useEffect(() => {\n if (!active) return undefined;\n const listener = (e: MouseEvent | TouchEvent) => {\n if (!ref.current || ref.current.contains(e.target as Node)) return;\n handler();\n };\n document.addEventListener('mousedown', listener);\n document.addEventListener('touchstart', listener);\n return () => {\n document.removeEventListener('mousedown', listener);\n document.removeEventListener('touchstart', listener);\n };\n }, [ref, handler, active]);\n}\n","interface SpinnerProps {\n size?: number;\n className?: string;\n}\n\nexport const Spinner = ({ size = 16, className = '' }: SpinnerProps) => (\n <div\n className={`animate-spin rounded-full border-2 border-current/20 border-t-[var(--chat-accent)] ${className}`}\n style={{ width: size, height: size }}\n />\n);\n","import { useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { findChatbotRoot } from '../utils';\n\ninterface TooltipProps {\n title: string;\n children: React.ReactElement;\n}\n\n// Approximate rendered tooltip height (text-xs + py-1) plus the 4px gap.\n// Used to decide whether a top-placed tooltip would overflow the panel.\nconst TOOLTIP_CLEARANCE = 28;\n\nexport const Tooltip = ({ title, children }: TooltipProps) => {\n const ref = useRef<HTMLSpanElement>(null);\n const [show, setShow] = useState(false);\n const [pos, setPos] = useState({ top: 0, left: 0 });\n const [below, setBelow] = useState(false);\n\n if (!title) return children;\n\n const handleEnter = () => {\n if (!ref.current) return;\n const rect = ref.current.getBoundingClientRect();\n // Flip below the anchor when a top-placed tooltip would extend above the\n // chatbot panel's top edge. The tooltip lives inside the panel's stacking\n // context (z-[1200] in sidebar mode), so anything drawn above the panel\n // lands in the host app's top-bar zone and is hidden whenever the host\n // bar stacks higher (e.g. OpenAEV's AppBar at theme.zIndex.drawer + 1).\n const rootTop = findChatbotRoot(ref.current).getBoundingClientRect().top;\n const flip = rect.top - rootTop < TOOLTIP_CLEARANCE;\n setBelow(flip);\n setPos({\n top: flip ? rect.bottom + 4 : rect.top - 4,\n left: rect.left + rect.width / 2,\n });\n setShow(true);\n };\n\n return (\n <span ref={ref} className=\"inline-flex\" onMouseEnter={handleEnter} onMouseLeave={() => setShow(false)}>\n {children}\n {show &&\n createPortal(\n <span\n className={`pointer-events-none fixed z-[10001] -translate-x-1/2 ${below ? '' : '-translate-y-full'} whitespace-nowrap rounded-md bg-gray-900 dark:bg-gray-100 px-2 py-1 text-xs text-white dark:text-gray-900 shadow-lg`}\n style={{ top: pos.top, left: pos.left }}\n role=\"tooltip\"\n >\n {title}\n </span>,\n findChatbotRoot(ref.current),\n )}\n </span>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport type { ChatConversationSummary, ChatMode, XtmAgent } from '../types';\nimport { timeAgo } from '../utils';\nimport {\n AlertTriangleIcon,\n ChevronDownIcon,\n CloseIcon,\n EditIcon,\n ExternalLinkIcon,\n FloatingIcon,\n FullscreenExitIcon,\n FullscreenIcon,\n HistoryIcon,\n SearchIcon,\n SidebarIcon,\n TrashIcon,\n UserPlusIcon,\n} from './icons';\nimport { Dropdown } from './Dropdown';\nimport { Spinner } from './Spinner';\nimport { Tooltip } from './Tooltip';\n\ninterface ChatHeaderProps {\n mode: ChatMode;\n agentName: string;\n agents: XtmAgent[];\n agentsLoading?: boolean;\n agentsError?: boolean;\n selectedAgent: XtmAgent | null;\n transferredFrom?: string;\n agentMenuOpen: boolean;\n onAgentMenuToggle: () => void;\n onAgentMenuClose: () => void;\n onSwitchAgent: (agent: XtmAgent) => void;\n modeMenuOpen: boolean;\n onModeMenuToggle: () => void;\n onModeMenuClose: () => void;\n onModeChange: (mode: ChatMode) => void;\n onNewChat: () => void;\n onClose: () => void;\n logoIcon: React.ReactNode;\n agentDashboardUrl?: string;\n /** Multi-conversation history menu (REST backend). Hidden when false. */\n historyEnabled?: boolean;\n historyMenuOpen?: boolean;\n onHistoryMenuToggle?: () => void;\n onHistoryMenuClose?: () => void;\n conversations?: ChatConversationSummary[];\n conversationsLoading?: boolean;\n activeConversationId?: string | null;\n onSelectConversation?: (id: string) => void;\n onDeleteConversation?: (id: string) => void;\n t: (key: string) => string;\n}\n\nconst modeOptions: { mode: ChatMode; label: string; getIcon: (p: { size: number; className: string }) => React.ReactNode }[] = [\n { mode: 'floating', label: 'Floating', getIcon: (p) => <FloatingIcon {...p} /> },\n { mode: 'sidebar', label: 'Sidebar', getIcon: (p) => <SidebarIcon {...p} /> },\n { mode: 'fullscreen', label: 'Full screen', getIcon: (p) => <FullscreenIcon {...p} /> },\n];\n\nexport const ChatHeader = ({\n mode,\n agentName,\n agents,\n agentsLoading = false,\n agentsError = false,\n selectedAgent,\n transferredFrom,\n agentMenuOpen,\n onAgentMenuToggle,\n onAgentMenuClose,\n onSwitchAgent,\n modeMenuOpen,\n onModeMenuToggle,\n onModeMenuClose,\n onModeChange,\n onNewChat,\n onClose,\n logoIcon,\n agentDashboardUrl,\n historyEnabled = false,\n historyMenuOpen = false,\n onHistoryMenuToggle,\n onHistoryMenuClose,\n conversations = [],\n conversationsLoading = false,\n activeConversationId = null,\n onSelectConversation,\n onDeleteConversation,\n t,\n}: ChatHeaderProps) => {\n const agentAnchorRef = useRef<HTMLButtonElement>(null);\n const modeAnchorRef = useRef<HTMLButtonElement>(null);\n const historyAnchorRef = useRef<HTMLButtonElement>(null);\n\n // Agent filter, mirroring the XTM One web chat's picker. Reset whenever the\n // menu closes so re-opening never starts on a stale query with most agents\n // hidden — which reads as \"my agents disappeared\".\n const [agentQuery, setAgentQuery] = useState('');\n useEffect(() => {\n if (!agentMenuOpen) setAgentQuery('');\n }, [agentMenuOpen]);\n\n // Match on name AND description: descriptions are what distinguish agents\n // whose names are near-identical, and they are already shown on every row.\n const filteredAgents = useMemo(() => {\n const q = agentQuery.trim().toLowerCase();\n if (!q) return agents;\n return agents.filter((a) => a.name.toLowerCase().includes(q) || (a.description ?? '').toLowerCase().includes(q));\n }, [agents, agentQuery]);\n\n // Only worth the vertical space once the list is long enough to scan for.\n const showAgentSearch = agents.length > 5;\n\n const CurrentModeIcon = mode === 'sidebar' ? SidebarIcon : mode === 'fullscreen' ? FullscreenExitIcon : FloatingIcon;\n\n return (\n <div\n className={`flex items-center px-3 py-2 min-h-[48px] border-b border-gray-200 dark:border-white/10 bg-gradient-to-br from-[var(--chat-accent-dark)]/[0.13] to-[var(--chat-accent)]/[0.07] ${mode === 'floating' ? 'rounded-t-xl' : ''}`}\n >\n <div className=\"min-w-0\">\n <button\n ref={agentAnchorRef}\n type=\"button\"\n onClick={onAgentMenuToggle}\n className=\"flex items-center gap-1.5 text-sm font-semibold text-gray-900 dark:text-white px-2 py-1 rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <span className=\"flex items-center text-[var(--chat-accent)] [&>svg]:w-[18px] [&>svg]:h-[18px]\">{logoIcon}</span>\n <span>{agentName}</span>\n <ChevronDownIcon size={16} className=\"text-gray-400 dark:text-white/30\" />\n </button>\n {transferredFrom && (\n <div className=\"pl-10 pr-2 text-[0.6rem] font-normal text-gray-400 dark:text-white/30\">\n {t('Transferred from')} {transferredFrom}\n </div>\n )}\n </div>\n\n <Dropdown open={agentMenuOpen} onClose={onAgentMenuClose} anchorRef={agentAnchorRef} width={280}>\n <span className=\"block px-4 pt-3 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">\n {t('Switch to another agent')}\n </span>\n {/* Three distinct states, because an empty array alone cannot tell them\n apart — and treating \"failed\" as \"still loading\" is what left this\n menu spinning forever against an unreachable backend.\n\n The last one is NOT \"the catalogue is empty\": XTM One always seeds\n agents, so a working backend never answers with none. It is reached\n when the fetch is skipped altogether — `apiEndpoints.agents: null`,\n single-endpoint mode (OpenCTI) or the legacy backend — where there\n is simply nothing to switch between. The menu still earns its place\n there: it carries the agent-dashboard links below. */}\n {agents.length === 0 && agentsLoading && (\n <div className=\"px-4 py-2\">\n <Spinner size={16} />\n </div>\n )}\n {agents.length === 0 && !agentsLoading && agentsError && (\n <div className=\"px-4 py-3 flex items-start gap-2\">\n <AlertTriangleIcon size={14} className=\"mt-0.5 shrink-0 text-amber-500 dark:text-amber-400\" />\n <span className=\"text-[0.75rem] leading-5 text-gray-600 dark:text-white/60\">{t('Could not reach the assistant service. Check the connection and try again.')}</span>\n </div>\n )}\n {agents.length === 0 && !agentsLoading && !agentsError && (\n <div className=\"px-4 py-3 text-[0.75rem] text-gray-400 dark:text-white/40\">{t('Agent switching is not available here')}</div>\n )}\n {showAgentSearch && (\n <div className=\"px-3 pb-2 pt-1\">\n <div className=\"relative\">\n <SearchIcon size={12} className=\"absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 dark:text-white/40\" />\n <input\n autoFocus\n type=\"text\"\n value={agentQuery}\n onChange={(e) => setAgentQuery(e.target.value)}\n // Escape closes the whole menu rather than only clearing the\n // query — the same key the rest of the panel uses to dismiss.\n onKeyDown={(e) => {\n if (e.key === 'Escape') onAgentMenuClose();\n }}\n placeholder={t('Search agents...')}\n aria-label={t('Search agents...')}\n className=\"w-full h-7 pl-7 pr-2 rounded-md bg-gray-100 dark:bg-white/[0.06] text-[0.75rem] text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-white/40 outline-hidden focus:ring-1 focus:ring-[var(--chat-accent)]\"\n />\n </div>\n </div>\n )}\n <div className=\"max-h-[240px] overflow-y-auto filigran-chat-scrollable\">\n {agents.length > 0 && filteredAgents.length === 0 && (\n <div className=\"px-4 py-3 text-[0.75rem] text-gray-400 dark:text-white/40\">{t('No agent matches')}</div>\n )}\n {filteredAgents.map((agent) => (\n <button\n key={agent.id}\n type=\"button\"\n onClick={() => onSwitchAgent(agent)}\n className={`w-full flex items-center gap-2 px-4 py-1.5 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors ${\n agent.id === selectedAgent?.id ? 'bg-[var(--chat-accent)]/10' : ''\n }`}\n >\n <div className=\"w-7 h-7 rounded-full flex items-center justify-center shrink-0 bg-gradient-to-br from-[var(--chat-accent)]/20 to-[var(--chat-accent)]/5\">\n <span className=\"text-[var(--chat-accent)] [&>svg]:w-4 [&>svg]:h-4\">{logoIcon}</span>\n </div>\n <div className=\"min-w-0\">\n <div className=\"text-[0.8125rem] font-medium text-gray-900 dark:text-white truncate\">{agent.name}</div>\n {agent.description && <div className=\"text-[0.7rem] text-gray-500 dark:text-white/40 truncate\">{agent.description}</div>}\n </div>\n </button>\n ))}\n </div>\n <div className=\"h-px bg-gray-200 dark:bg-white/10 mx-2\" />\n <div>\n {agentDashboardUrl && (\n <button\n type=\"button\"\n onClick={() => {\n onAgentMenuClose();\n window.open(`${agentDashboardUrl}/agents`, '_blank');\n }}\n className=\"w-full flex items-center gap-2 px-4 py-1.5 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <ExternalLinkIcon size={18} className=\"text-gray-400 dark:text-white/40 shrink-0\" />\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/70\">{t('Browse agents')}</span>\n </button>\n )}\n {agentDashboardUrl && (\n <button\n type=\"button\"\n onClick={() => {\n onAgentMenuClose();\n window.open(`${agentDashboardUrl}/agents/new`, '_blank');\n }}\n className=\"w-full flex items-center gap-2 px-4 py-1.5 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <UserPlusIcon size={18} className=\"text-gray-400 dark:text-white/40 shrink-0\" />\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/70\">{t('Create agent')}</span>\n </button>\n )}\n </div>\n </Dropdown>\n\n <div className=\"flex-1\" />\n\n {historyEnabled && (\n <>\n <Tooltip title={t('Conversation history')}>\n <button\n ref={historyAnchorRef}\n type=\"button\"\n onClick={onHistoryMenuToggle}\n aria-label={t('Conversation history')}\n aria-haspopup=\"menu\"\n aria-expanded={historyMenuOpen}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <HistoryIcon size={18} />\n </button>\n </Tooltip>\n\n <Dropdown open={historyMenuOpen} onClose={() => onHistoryMenuClose?.()} anchorRef={historyAnchorRef} placement=\"bottom-end\" width={300}>\n <span className=\"block px-4 pt-3 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">\n {t('Conversation history')}\n </span>\n <div className=\"max-h-72 overflow-y-auto filigran-chat-scrollable\">\n {conversationsLoading && conversations.length === 0 && (\n <div className=\"px-4 py-2\">\n <Spinner size={16} />\n </div>\n )}\n {!conversationsLoading && conversations.length === 0 && (\n <div className=\"px-4 py-3 text-[0.75rem] text-gray-400 dark:text-white/40\">{t('No conversations yet')}</div>\n )}\n {conversations.map((conv) => {\n const isActive = conv.conversationId === activeConversationId;\n const when = timeAgo(conv.updatedAt, t);\n return (\n <div\n key={conv.conversationId}\n className={`group flex items-center gap-2 px-4 py-1.5 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors ${\n isActive ? 'bg-[var(--chat-accent)]/10' : ''\n }`}\n >\n <button type=\"button\" onClick={() => onSelectConversation?.(conv.conversationId)} className=\"flex-1 min-w-0 text-left\">\n <div className=\"text-[0.8125rem] font-medium text-gray-900 dark:text-white truncate\">\n {conv.title || t('Untitled conversation')}\n </div>\n {when && <div className=\"text-[0.7rem] text-gray-500 dark:text-white/40 truncate\">{when}</div>}\n </button>\n {onDeleteConversation && (\n <button\n type=\"button\"\n onClick={() => onDeleteConversation(conv.conversationId)}\n title={t('Delete conversation')}\n aria-label={t('Delete conversation')}\n className=\"shrink-0 p-1 rounded-md text-gray-400 dark:text-white/30 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:text-red-500 dark:hover:text-red-400 transition-all\"\n >\n <TrashIcon size={14} />\n </button>\n )}\n </div>\n );\n })}\n </div>\n <div className=\"h-px bg-gray-200 dark:bg-white/10 mx-2\" />\n <button\n type=\"button\"\n onClick={() => {\n onHistoryMenuClose?.();\n onNewChat();\n }}\n className=\"w-full flex items-center gap-2 px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <EditIcon size={16} className=\"text-gray-400 dark:text-white/40 shrink-0\" />\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/70\">{t('New conversation')}</span>\n </button>\n </Dropdown>\n </>\n )}\n\n <Tooltip title={t('New chat')}>\n <button\n type=\"button\"\n onClick={onNewChat}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <EditIcon size={18} />\n </button>\n </Tooltip>\n\n <Tooltip title={t('Switch view')}>\n <button\n ref={modeAnchorRef}\n type=\"button\"\n onClick={onModeMenuToggle}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <CurrentModeIcon size={18} />\n </button>\n </Tooltip>\n\n <Dropdown open={modeMenuOpen} onClose={onModeMenuClose} anchorRef={modeAnchorRef} placement=\"bottom-end\" width={180}>\n <span className=\"block px-4 pt-3 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">{t('Switch to')}</span>\n <div className=\"pb-1\">\n {modeOptions.map((opt) => (\n <button\n key={opt.mode}\n type=\"button\"\n onClick={() => {\n onModeChange(opt.mode);\n onModeMenuClose();\n }}\n className={`w-full flex items-center gap-3 px-4 py-1 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors ${\n mode === opt.mode ? 'bg-[var(--chat-accent)]/10' : ''\n }`}\n >\n {opt.getIcon({ size: 18, className: 'text-gray-400 dark:text-white/40' })}\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/70\">{t(opt.label)}</span>\n </button>\n ))}\n </div>\n </Dropdown>\n\n <Tooltip title={t('Close')}>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <CloseIcon size={18} />\n </button>\n </Tooltip>\n </div>\n );\n};\n","import { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Minimal shape of the Web Speech API surface we use. Typed locally rather\n * than pulled from `lib.dom` — `SpeechRecognition` is still vendor-prefixed and\n * missing from TypeScript's DOM lib, and this package ships no ambient types.\n */\ninterface SpeechRecognitionAlternativeLike {\n transcript: string;\n}\ninterface SpeechRecognitionResultLike {\n isFinal: boolean;\n 0: SpeechRecognitionAlternativeLike;\n}\ninterface SpeechRecognitionEventLike {\n resultIndex: number;\n results: { length: number; [index: number]: SpeechRecognitionResultLike };\n}\ninterface SpeechRecognitionLike {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n start: () => void;\n stop: () => void;\n onresult: ((event: SpeechRecognitionEventLike) => void) | null;\n onerror: (() => void) | null;\n onend: (() => void) | null;\n}\ntype SpeechRecognitionCtor = new () => SpeechRecognitionLike;\n\nfunction getSpeechRecognition(): SpeechRecognitionCtor | null {\n if (typeof window === 'undefined') return null;\n const w = window as unknown as {\n SpeechRecognition?: SpeechRecognitionCtor;\n webkitSpeechRecognition?: SpeechRecognitionCtor;\n };\n return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;\n}\n\ninterface UseDictationReturn {\n /** False on browsers without the API — the host should render no button. */\n supported: boolean;\n listening: boolean;\n /** Words heard but not yet finalised, for a live preview. */\n interim: string;\n toggle: () => void;\n stop: () => void;\n}\n\n/**\n * Speech-to-text for the composer, using the browser's own Web Speech API.\n *\n * Entirely client-side: no endpoint, no key, nothing for a host to deploy —\n * which is why it ships regardless of how the backend is configured, unlike the\n * prompt library and quota indicator.\n *\n * Final phrases are appended through `onFinalText`; interim words are returned\n * separately so the composer can preview them without committing.\n */\nexport function useDictation(onFinalText: (text: string) => void): UseDictationReturn {\n const [listening, setListening] = useState(false);\n const [interim, setInterim] = useState('');\n const recognitionRef = useRef<SpeechRecognitionLike | null>(null);\n // Recognition ends on its own after a pause; this distinguishes \"the browser\n // stopped listening\" from \"the user asked to stop\", so a natural pause\n // mid-sentence does not silently end the session.\n const shouldRestartRef = useRef(false);\n // Read through a ref so re-creating the callback each render does not tear\n // down and rebuild the recogniser mid-dictation.\n const onFinalTextRef = useRef(onFinalText);\n onFinalTextRef.current = onFinalText;\n\n const supported = getSpeechRecognition() !== null;\n\n useEffect(() => {\n const Ctor = getSpeechRecognition();\n if (!Ctor) return;\n\n const recognition = new Ctor();\n recognition.continuous = true;\n recognition.interimResults = true;\n recognition.lang = typeof navigator !== 'undefined' ? navigator.language || 'en-US' : 'en-US';\n\n recognition.onresult = (event) => {\n let interimText = '';\n let finalText = '';\n for (let i = event.resultIndex; i < event.results.length; i++) {\n const result = event.results[i];\n if (result.isFinal) finalText += result[0].transcript;\n else interimText += result[0].transcript;\n }\n if (finalText) {\n onFinalTextRef.current(finalText);\n setInterim('');\n } else {\n setInterim(interimText);\n }\n };\n\n recognition.onerror = () => {\n // Permission denied, no microphone, network failure — stop cleanly\n // rather than leaving the button stuck in its listening state.\n shouldRestartRef.current = false;\n setListening(false);\n setInterim('');\n };\n\n recognition.onend = () => {\n if (shouldRestartRef.current) {\n try {\n recognition.start();\n return;\n } catch {\n /* already starting, or the engine refused — fall through and stop */\n }\n }\n setListening(false);\n setInterim('');\n };\n\n recognitionRef.current = recognition;\n return () => {\n shouldRestartRef.current = false;\n recognitionRef.current = null;\n try {\n recognition.stop();\n } catch {\n /* never started */\n }\n };\n }, []);\n\n const stop = useCallback(() => {\n shouldRestartRef.current = false;\n setInterim('');\n setListening(false);\n try {\n recognitionRef.current?.stop();\n } catch {\n /* already stopped */\n }\n }, []);\n\n const toggle = useCallback(() => {\n const recognition = recognitionRef.current;\n if (!recognition) return;\n if (shouldRestartRef.current) {\n stop();\n return;\n }\n try {\n shouldRestartRef.current = true;\n recognition.start();\n setListening(true);\n } catch {\n // `start()` throws if it is already running; treat that as \"not started\"\n // rather than leaving the UI claiming to listen.\n shouldRestartRef.current = false;\n setListening(false);\n }\n }, [stop]);\n\n return { supported, listening, interim, toggle, stop };\n}\n","import { useRef, useState } from 'react';\nimport type { ChatContextBreakdown, ChatContextUsage } from '../types';\nimport { compactCount } from '../utils';\nimport { Dropdown } from './Dropdown';\nimport { Tooltip } from './Tooltip';\n\ninterface ContextUsageIndicatorProps {\n usage: ChatContextUsage;\n t: (key: string) => string;\n}\n\n// Thresholds are the agent loop's own gates, not design choices: at 80 %\n// utilization the backend compacts the session (older turns are distilled into\n// a summary), and past 95 % it emergency-prunes. Colouring anywhere else would\n// warn about a moment that never comes, or arrive after it has passed.\nconst COMPACTION_RATIO = 0.8;\nconst PRUNE_RATIO = 0.95;\n\n// Geometry of the ring. Kept at the text's own scale so the gauge reads as part\n// of the label rather than as an icon beside it.\nconst SIZE = 14;\nconst STROKE = 2;\nconst RADIUS = (SIZE - STROKE) / 2;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\n/**\n * Rows of the detail popover, in a FIXED order that mirrors how the prompt is\n * assembled — instructions, then tools, then the conversation and what the\n * backend has done to it. Fixed rather than sorted by size so the same bucket\n * stays in the same place as a chat grows: a legend whose rows reshuffle between\n * two glances cannot be compared against itself.\n *\n * `color` is both the swatch and the stacked-bar segment, so the bar is readable\n * without a legend lookup. Labels name what the user can act on, not the\n * backend's internals: \"Tool results\" rather than \"role=tool messages\".\n */\nconst ROWS: ReadonlyArray<{ field: keyof ChatContextBreakdown; label: string; color: string }> = [\n { field: 'system', label: 'System prompt', color: '#9ca3af' },\n { field: 'tools', label: 'Tool definitions', color: '#a78bfa' },\n { field: 'dynamicTools', label: 'MCP & dynamic tools', color: '#f0abfc' },\n { field: 'summary', label: 'Summarized conversation', color: '#fb7185' },\n { field: 'toolResults', label: 'Tool results', color: '#34d399' },\n { field: 'conversation', label: 'Conversation', color: '#a1a1aa' },\n];\n\n/**\n * Context-window occupancy for the current conversation, as a small ring plus\n * percentage — the affordance Cursor popularised — opening a breakdown of where\n * the context went.\n *\n * It answers one question: is this conversation about to get shorter than the\n * user thinks? Long chats do not fail at the window, they get silently\n * summarised, and a user who cannot see that coming reads the summary's gaps as\n * the assistant forgetting. So the gauge is deliberately a *forecast* of the\n * next turn, and its colours are the backend's real thresholds.\n */\nexport const ContextUsageIndicator = ({ usage, t }: ContextUsageIndicatorProps) => {\n const { used, limit, breakdown } = usage;\n const anchorRef = useRef<HTMLButtonElement>(null);\n const [open, setOpen] = useState(false);\n\n // The parser guarantees a positive limit; clamp anyway so a future producer\n // cannot draw a ring past full or a negative arc.\n const ratio = Math.min(Math.max(used / limit, 0), 1);\n const pruning = ratio >= PRUNE_RATIO;\n const compacting = ratio >= COMPACTION_RATIO;\n\n const ringColor = pruning ? 'text-red-500' : compacting ? 'text-amber-500' : 'text-[var(--chat-accent)]';\n const textColor = pruning\n ? 'text-red-500 dark:text-red-400'\n : compacting\n ? 'text-amber-600 dark:text-amber-400'\n : 'text-gray-400 dark:text-white/30';\n\n const percent = Math.round(ratio * 100);\n const counts = `${compactCount(used)}/${compactCount(limit)}`;\n // Naming the consequence beats naming the state: \"80 % full\" leaves the user\n // to guess what happens next, which is the whole reason the gauge exists.\n const headline = pruning\n ? t('Context full — older turns are being dropped')\n : compacting\n ? t('Context nearly full — older turns are being summarized')\n : t('Context used');\n const summary = `${headline} · ${counts} ${t('tokens')}`;\n\n const rows = breakdown ? ROWS.filter((r) => (breakdown[r.field] ?? 0) > 0) : [];\n // Without a breakdown there is nothing to open, so the readout stays inert\n // rather than offering a click that does nothing.\n const expandable = rows.length > 0;\n\n const gauge = (\n <span className=\"flex items-center gap-1.5\">\n <svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`} className={ringColor} aria-hidden=\"true\">\n <circle\n cx={SIZE / 2}\n cy={SIZE / 2}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n className=\"stroke-gray-200 dark:stroke-white/15\"\n />\n <circle\n cx={SIZE / 2}\n cy={SIZE / 2}\n r={RADIUS}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n strokeDasharray={CIRCUMFERENCE}\n strokeDashoffset={CIRCUMFERENCE * (1 - ratio)}\n /* Start at twelve o'clock: a gauge that fills from the side reads\n as a spinner. */\n transform={`rotate(-90 ${SIZE / 2} ${SIZE / 2})`}\n className=\"transition-[stroke-dashoffset] duration-500\"\n />\n </svg>\n <span className={`text-[0.68rem] tabular-nums ${textColor}`}>{percent}%</span>\n </span>\n );\n\n if (!expandable) {\n return (\n <Tooltip title={summary}>\n <span className=\"flex items-center\" role=\"img\" aria-label={`${summary} (${percent}%)`}>\n {gauge}\n </span>\n </Tooltip>\n );\n }\n\n return (\n <>\n <Tooltip title={open ? '' : `${summary} — ${t('click for details')}`}>\n <button\n ref={anchorRef}\n type=\"button\"\n onClick={() => setOpen((prev) => !prev)}\n aria-label={`${summary} (${percent}%)`}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className=\"flex items-center rounded-md px-1 -mx-1 py-0.5 transition-colors hover:bg-gray-100 dark:hover:bg-white/10\"\n >\n {gauge}\n </button>\n </Tooltip>\n\n <Dropdown open={open} onClose={() => setOpen(false)} anchorRef={anchorRef} placement=\"bottom-end\" width={296}>\n <div className=\"px-3.5 pt-3 pb-1 flex items-baseline justify-between gap-3\">\n <span className={`text-[0.8125rem] tabular-nums ${textColor}`}>{t('{percent}% full').replace('{percent}', String(percent))}</span>\n {/* The tilde is load-bearing: these are char-derived estimates, and a\n bare \"168k/200k\" would read as a measured count. */}\n <span className=\"text-[0.7rem] tabular-nums text-gray-400 dark:text-white/40 shrink-0\">\n ~{counts} {t('tokens')}\n </span>\n </div>\n\n {/* One stacked bar over the whole window: the segments are the legend's\n own colours, so proportions are readable without matching numbers to\n rows. The trailing gap is the headroom left. */}\n <div className=\"px-3.5 pb-2.5 pt-1.5\">\n <span className=\"flex h-1.5 w-full gap-px rounded-full bg-gray-200 dark:bg-white/10 overflow-hidden\">\n {rows.map((row) => (\n <span\n key={row.field}\n className=\"h-full first:rounded-l-full\"\n /* Scaled through the clamped total rather than straight over the\n window, so a producer that reports buckets summing past the\n limit fills the track instead of overflowing it. Identical to\n value/limit whenever the two agree, which is the normal case. */\n style={{\n width: `${((breakdown?.[row.field] ?? 0) / Math.max(used, 1)) * Math.min(used / limit, 1) * 100}%`,\n backgroundColor: row.color,\n }}\n />\n ))}\n </span>\n </div>\n\n <div className=\"px-3.5 pb-1\">\n {rows.map((row) => (\n <div key={row.field} className=\"flex items-center gap-2 py-[3px]\">\n {/* Radius set inline: the panel's reset rounds small spans to a\n pill, which turns the swatches into dots that read as bullets\n rather than as keys to the bar's segments. */}\n <span className=\"h-2 w-2 shrink-0\" style={{ backgroundColor: row.color, borderRadius: 2 }} aria-hidden=\"true\" />\n <span className=\"text-[0.75rem] text-gray-700 dark:text-white/70 truncate\">{t(row.label)}</span>\n <span className=\"ml-auto text-[0.7rem] tabular-nums text-gray-500 dark:text-white/40 shrink-0\">{compactCount(breakdown?.[row.field] ?? 0)}</span>\n </div>\n ))}\n </div>\n\n {compacting && (\n // The one line that turns a readout into something actionable: at this\n // point the backend is already dropping detail from older turns.\n <p className={`px-3.5 pt-1 pb-3 text-[0.68rem] leading-snug ${textColor}`}>{headline}</p>\n )}\n </Dropdown>\n </>\n );\n};\n","import { useRef, useState } from 'react';\nimport type { ChatPromptTemplate } from '../types';\nimport { SparklesIcon } from './icons';\nimport { Dropdown } from './Dropdown';\nimport { Tooltip } from './Tooltip';\n\ninterface PromptPickerProps {\n prompts: ChatPromptTemplate[];\n /** Receives the template body; the composer decides how to place it. */\n onPick: (content: string) => void;\n t: (key: string) => string;\n}\n\n/** Above this many entries the list is worth filtering rather than scrolling. */\nconst SEARCH_THRESHOLD = 5;\n\n/**\n * Inserts a saved prompt template into the composer. Mirrors the XTM One web\n * chat's \"Insert prompt template\" affordance.\n */\nexport const PromptPicker = ({ prompts, onPick, t }: PromptPickerProps) => {\n const anchorRef = useRef<HTMLButtonElement>(null);\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState('');\n\n const close = () => {\n setOpen(false);\n // Reset on close: re-opening on a stale filter looks like the library\n // lost most of its entries.\n setQuery('');\n };\n\n const q = query.trim().toLowerCase();\n const filtered = q\n ? prompts.filter((p) => p.title.toLowerCase().includes(q) || (p.description ?? '').toLowerCase().includes(q))\n : prompts;\n\n return (\n <>\n <Tooltip title={t('Insert prompt template')}>\n <button\n ref={anchorRef}\n type=\"button\"\n onClick={() => (open ? close() : setOpen(true))}\n aria-label={t('Insert prompt template')}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n className={`w-7 h-7 flex items-center justify-center rounded-lg transition-colors ${\n open\n ? 'text-[var(--chat-accent)] bg-[var(--chat-accent)]/10'\n : 'text-gray-400 dark:text-white/30 hover:bg-gray-100 dark:hover:bg-white/10'\n }`}\n >\n <SparklesIcon size={15} />\n </button>\n </Tooltip>\n\n <Dropdown open={open} onClose={close} anchorRef={anchorRef} width={280}>\n <span className=\"block px-4 pt-3 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">\n {t('Insert prompt template')}\n </span>\n\n {prompts.length > SEARCH_THRESHOLD && (\n <div className=\"px-3 pb-2 pt-1\">\n <input\n autoFocus\n type=\"text\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape') close();\n }}\n placeholder={t('Search prompts...')}\n aria-label={t('Search prompts...')}\n className=\"w-full h-7 px-2 rounded-md bg-gray-100 dark:bg-white/[0.06] text-[0.75rem] text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-white/40 outline-hidden focus:ring-1 focus:ring-[var(--chat-accent)]\"\n />\n </div>\n )}\n\n <div className=\"max-h-[240px] overflow-y-auto filigran-chat-scrollable\">\n {filtered.length === 0 && <div className=\"px-4 py-3 text-[0.75rem] text-gray-400 dark:text-white/40\">{t('No prompt matches')}</div>}\n {filtered.map((p) => (\n <button\n key={p.id}\n type=\"button\"\n onClick={() => {\n onPick(p.content);\n close();\n }}\n className=\"w-full px-4 py-1.5 text-left hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <div className=\"text-[0.8125rem] text-gray-900 dark:text-white truncate\">{p.title}</div>\n {p.description && <div className=\"text-[0.7rem] text-gray-500 dark:text-white/40 truncate\">{p.description}</div>}\n </button>\n ))}\n </div>\n </Dropdown>\n </>\n );\n};\n","import type { ChatQuotaStatus } from '../types';\nimport { compactCount as compact } from '../utils';\nimport { Tooltip } from './Tooltip';\n\ninterface QuotaIndicatorProps {\n quota: ChatQuotaStatus;\n t: (key: string) => string;\n}\n\n/**\n * Agentic quota headroom, as a small bar plus counts.\n *\n * Colour is earned, not decorative: neutral until 75%, amber past it, red once\n * the allowance is spent — the point is to warn before a turn is refused, not\n * to decorate the composer.\n */\nexport const QuotaIndicator = ({ quota, t }: QuotaIndicatorProps) => {\n const { used, limit, period } = quota;\n\n // No ceiling: report consumption without implying a limit that isn't there.\n if (limit === null) {\n return (\n <Tooltip title={period ? `${t('Usage')} · ${period}` : t('Usage')}>\n <span className=\"text-[0.68rem] tabular-nums text-gray-400 dark:text-white/30\">{compact(used)}</span>\n </Tooltip>\n );\n }\n\n // Guard a zero/negative limit rather than dividing by it.\n const ratio = limit > 0 ? Math.min(used / limit, 1) : 1;\n const exhausted = limit > 0 ? used >= limit : true;\n const nearLimit = ratio >= 0.75;\n\n const barColor = exhausted ? 'bg-red-500' : nearLimit ? 'bg-amber-500' : 'bg-[var(--chat-accent)]/60';\n const textColor = exhausted\n ? 'text-red-500 dark:text-red-400'\n : nearLimit\n ? 'text-amber-600 dark:text-amber-400'\n : 'text-gray-400 dark:text-white/30';\n\n const label = `${compact(used)}/${compact(limit)}`;\n const title = [exhausted ? t('Quota reached') : t('Quota'), period].filter(Boolean).join(' · ');\n\n return (\n <Tooltip title={title}>\n <span className=\"flex items-center gap-1.5\" role=\"img\" aria-label={`${title} ${label}`}>\n <span className=\"h-1 w-10 rounded-full bg-gray-200 dark:bg-white/10 overflow-hidden\">\n <span className={`block h-full rounded-full transition-[width] duration-300 ${barColor}`} style={{ width: `${ratio * 100}%` }} />\n </span>\n <span className={`text-[0.68rem] tabular-nums ${textColor}`}>{label}</span>\n </span>\n </Tooltip>\n );\n};\n","import { useRef, type KeyboardEvent } from 'react';\nimport type { ChatContextUsage, ChatFile, ChatMode, ChatPromptTemplate, ChatQuotaStatus } from '../types';\nimport { AttachFileIcon, FileIcon, MicIcon, MicOffIcon, SendIcon, StopCircleIcon } from './icons';\nimport { useDictation } from '../hooks/useDictation';\nimport { ContextUsageIndicator } from './ContextUsageIndicator';\nimport { PromptPicker } from './PromptPicker';\nimport { QuotaIndicator } from './QuotaIndicator';\nimport { Tooltip } from './Tooltip';\n\ninterface ChatInputProps {\n inputValue: string;\n onInputChange: (value: string) => void;\n onSend: () => void;\n onStop: () => void;\n isLoading: boolean;\n /**\n * Mid-run steering availability: while the agent is generating, the typed\n * text can be dispatched immediately (Enter / accent Send button) and is\n * injected into the running run instead of waiting for it to finish.\n * Attachments keep the legacy wait behavior.\n */\n canSteer?: boolean;\n attachedFiles?: ChatFile[];\n onFileAdd?: (files: FileList | null) => void;\n onFileRemove?: (index: number) => void;\n onPaste?: (e: React.ClipboardEvent) => void;\n t: (key: string) => string;\n mode?: ChatMode;\n separatorColor?: string;\n /** Saved prompt templates; omitted entirely when the host serves none. */\n prompts?: ChatPromptTemplate[] | null;\n /** Agentic quota headroom; omitted entirely when the host serves none. */\n quota?: ChatQuotaStatus | null;\n /** Context-window occupancy; omitted until the backend reports it. */\n contextUsage?: ChatContextUsage | null;\n /** Host-supplied controls appended to the toolbar (see `composerToolbar`). */\n composerToolbar?: React.ReactNode;\n}\n\nexport const ChatInput = ({\n inputValue,\n onInputChange,\n onSend,\n onStop,\n isLoading,\n canSteer = false,\n attachedFiles = [],\n onFileAdd,\n onFileRemove,\n onPaste,\n t,\n mode,\n separatorColor,\n prompts,\n quota,\n contextUsage,\n composerToolbar,\n}: ChatInputProps) => {\n const fileInputRef = useRef<HTMLInputElement>(null);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n // Leaving the mic live after a send would splice the next words into a\n // composer the user believes they just emptied.\n dictation.stop();\n onSend();\n }\n if (e.key === 'Escape' && isLoading) {\n e.preventDefault();\n onStop();\n }\n };\n\n const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n onInputChange(e.target.value);\n const el = e.target;\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, 120)}px`;\n };\n\n // Append rather than replace: a user who has already started typing must not\n // lose it to a template pick. The blank line keeps the two blocks distinct.\n const handlePromptPick = (content: string) => {\n onInputChange(inputValue.trim() ? `${inputValue.trimEnd()}\\n\\n${content}` : content);\n textareaRef.current?.focus();\n };\n\n // Dictation appends each finalised phrase, so speaking continues a draft\n // rather than replacing it — same contract as picking a template.\n const dictation = useDictation((finalText) => {\n onInputChange(inputValue.trim() ? `${inputValue.trimEnd()} ${finalText}` : finalText);\n });\n\n // The toolbar row costs vertical space, so it only exists when something\n // actually occupies it.\n const hasToolbar = Boolean((prompts && prompts.length > 0) || quota || contextUsage || composerToolbar || dictation.supported);\n\n const isFileManagementEnabled = Boolean(onFileAdd && onFileRemove && onPaste);\n const hasContent = inputValue.trim() || (isFileManagementEnabled && attachedFiles.length > 0);\n const hasFilesUploading = isFileManagementEnabled && attachedFiles.some((f) => f.uploadStatus === 'pending');\n const canSend = hasContent && !hasFilesUploading;\n const hasAttachments = isFileManagementEnabled && attachedFiles.length > 0;\n // Show the accent Send button NEXT to Stop while generating: text-only\n // sends can steer the running agent. With attachments selected the send\n // must wait for the current response, so only Stop is shown.\n const showSteerSend = isLoading && canSteer && Boolean(inputValue.trim()) && !hasAttachments;\n\n const footerText =\n isLoading && canSteer && !hasAttachments\n ? t('Enter to send now · Esc to stop')\n : isLoading && hasAttachments\n ? t('Attachments wait for the current response')\n : t('Uses AI. Verify results.');\n\n return (\n <div\n className={`px-4 py-3 border-t border-gray-200 dark:border-white/10 ${mode === 'floating' ? 'rounded-b-xl' : ''}`}\n style={separatorColor ? { borderTopColor: separatorColor, borderTopWidth: 1 } : undefined}\n >\n {isFileManagementEnabled && attachedFiles.length > 0 && (\n <div className=\"flex gap-1.5 flex-wrap mb-2\">\n {attachedFiles.map((f, i) => (\n <span\n key={i}\n className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[0.7rem] ${\n f.uploadStatus === 'error'\n ? 'border-red-300 dark:border-red-500/30 text-red-500 dark:text-red-400'\n : f.uploadStatus === 'pending'\n ? 'border-gray-200 dark:border-white/10 text-gray-400 dark:text-white/40'\n : 'border-gray-200 dark:border-white/10 text-gray-600 dark:text-white/60'\n }`}\n >\n {f.uploadStatus === 'pending' ? (\n <span className=\"w-3.5 h-3.5 border border-current/30 border-t-current rounded-full animate-spin\" />\n ) : (\n <FileIcon size={14} />\n )}\n {f.name}\n {f.uploadStatus === 'error' && <span className=\"text-red-400 text-[0.6rem]\">✕</span>}\n <button\n type=\"button\"\n onClick={() => onFileRemove?.(i)}\n className=\"ml-0.5 text-gray-400 dark:text-white/30 hover:text-gray-600 dark:hover:text-white/60\"\n >\n ×\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div className=\"flex items-center border border-gray-200 dark:border-white/10 rounded-xl px-2 py-1 transition-colors focus-within:border-[var(--chat-accent)]\">\n {isFileManagementEnabled && (\n <>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n hidden\n onChange={(e) => {\n onFileAdd?.(e.target.files);\n e.target.value = '';\n }}\n />\n <button\n type=\"button\"\n onClick={() => fileInputRef.current?.click()}\n className=\"w-8 h-8 flex items-center justify-center shrink-0 rounded-lg text-gray-400 dark:text-white/30 hover:bg-gray-100 dark:hover:bg-white/10 mr-0.5 transition-colors\"\n >\n <AttachFileIcon size={18} />\n </button>\n </>\n )}\n <textarea\n ref={textareaRef}\n placeholder={t('Ask a question...')}\n value={inputValue}\n onChange={handleInput}\n onKeyDown={handleKeyDown}\n onPaste={onPaste}\n rows={1}\n className=\"flex-1 bg-transparent border-none outline-hidden resize-none text-[0.8125rem] py-1.5 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-white/30 filigran-chat-scrollable\"\n style={{ maxHeight: 120 }}\n />\n {showSteerSend && (\n <Tooltip title={t('Send now')}>\n <button\n type=\"button\"\n onClick={onSend}\n aria-label={t('Send now')}\n className=\"p-1.5 rounded-lg w-8 h-8 flex items-center justify-center transition-all duration-150 text-[var(--chat-accent)] bg-[var(--chat-accent)]/10 hover:bg-[var(--chat-accent)]/20\"\n >\n <SendIcon size={18} />\n </button>\n </Tooltip>\n )}\n <Tooltip title={isLoading ? t('Stop generating') : hasFilesUploading ? t('Files uploading...') : ''}>\n <button\n type=\"button\"\n onClick={isLoading ? onStop : onSend}\n disabled={!isLoading && !canSend}\n className={`p-1.5 rounded-lg w-8 h-8 flex items-center justify-center transition-all duration-150 ${\n isLoading\n ? 'text-red-500 bg-red-500/10 hover:bg-red-500/20 ml-0.5'\n : canSend\n ? 'text-[var(--chat-accent)] bg-[var(--chat-accent)]/10 hover:bg-[var(--chat-accent)]/20'\n : 'text-gray-300 dark:text-white/20 cursor-not-allowed'\n }`}\n >\n {isLoading ? <StopCircleIcon size={18} /> : <SendIcon size={18} />}\n </button>\n </Tooltip>\n </div>\n\n {hasToolbar && (\n <div className=\"flex items-center gap-1.5 mt-1.5 px-0.5\">\n {prompts && prompts.length > 0 && <PromptPicker prompts={prompts} onPick={handlePromptPick} t={t} />}\n {dictation.supported && (\n <Tooltip title={dictation.listening ? t('Stop dictation') : t('Dictate a message')}>\n <button\n type=\"button\"\n onClick={dictation.toggle}\n aria-label={dictation.listening ? t('Stop dictation') : t('Dictate a message')}\n aria-pressed={dictation.listening}\n className={`w-7 h-7 flex items-center justify-center rounded-lg transition-colors ${\n dictation.listening\n ? 'text-red-500 bg-red-500/10 hover:bg-red-500/20'\n : 'text-gray-400 dark:text-white/30 hover:bg-gray-100 dark:hover:bg-white/10'\n }`}\n >\n {dictation.listening ? <MicOffIcon size={15} /> : <MicIcon size={15} />}\n </button>\n </Tooltip>\n )}\n {dictation.interim && (\n <span className=\"text-[0.7rem] italic text-gray-400 dark:text-white/30 truncate max-w-[45%]\">{dictation.interim}</span>\n )}\n {composerToolbar}\n {/* Status readouts sit last and pushed right, together: they are not\n controls, so neither should ever land between two clickable\n things. Context before quota — \"how full is this chat\" is the one\n the user can still act on by starting a new one. */}\n {(contextUsage || quota) && (\n <span className=\"ml-auto flex items-center gap-2.5\">\n {contextUsage && <ContextUsageIndicator usage={contextUsage} t={t} />}\n {quota && <QuotaIndicator quota={quota} t={t} />}\n </span>\n )}\n </div>\n )}\n\n <p className=\"text-center text-[0.65rem] text-gray-400 dark:text-white/30 mt-1.5 opacity-70\">{footerText}</p>\n </div>\n );\n};\n","import { useEffect, useId, useRef, useState } from 'react';\nimport type { ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict } from '../types';\nimport { AlertTriangleIcon, CheckIcon, WrenchIcon, XCircleIcon } from './icons';\n\ninterface ChatApprovalPromptProps {\n proposals: ToolApprovalProposal[];\n /** Sends one decision per proposal — the backend refuses a partial set. */\n onSubmit: (decisions: ToolApprovalDecision[]) => void;\n isSubmitting?: boolean;\n /** Why the last submission failed. Non-null re-arms the controls for a retry. */\n error?: string | null;\n t: (key: string) => string;\n}\n\n/** Shape of one argument as described by the tool's own JSON Schema. */\ninterface ArgumentSchema {\n description?: string;\n type?: string;\n}\n\n/**\n * Pull `{ description, type }` for one argument out of a JSON Schema, tolerating\n * a schema that is absent or shaped unexpectedly — an unlabelled argument still\n * renders, it just carries less.\n */\nfunction argumentSchema(inputSchema: Record<string, unknown> | undefined, name: string): ArgumentSchema {\n const properties = inputSchema?.properties;\n if (!properties || typeof properties !== 'object') return {};\n const entry = (properties as Record<string, unknown>)[name];\n if (!entry || typeof entry !== 'object') return {};\n const e = entry as Record<string, unknown>;\n return {\n description: typeof e.description === 'string' ? e.description : undefined,\n type: typeof e.type === 'string' ? e.type : undefined,\n };\n}\n\n/** Render an argument value as something a person can read. */\nfunction formatValue(value: unknown): string {\n if (typeof value === 'string') return value;\n try {\n return JSON.stringify(value, null, 2) ?? String(value);\n } catch {\n // Cyclic or otherwise unstringifiable — the reviewer still needs to see\n // that the argument is there, so degrade rather than blank the card.\n return String(value);\n }\n}\n\n/**\n * Scroll `ref` into view when `active` becomes true, and on each later\n * transition into it.\n *\n * Every control here is revealed rather than always present: the prompt itself\n * arrives at the bottom of a thread that has just grown by a whole turn, \"No\"\n * opens a reason box under the card, and \"Yes, always\" opens a warning under the\n * list. In each case the click can look like it did nothing — and on a *paused*\n * turn a stalled reviewer stalls the agent too. rAF because the revealed node\n * must be laid out before it can be scrolled to.\n */\nfunction useRevealIntoView<T extends HTMLElement>(active: boolean) {\n const ref = useRef<T>(null);\n useEffect(() => {\n if (!active) return;\n const raf = requestAnimationFrame(() => {\n ref.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });\n });\n return () => cancelAnimationFrame(raf);\n }, [active]);\n return ref;\n}\n\nconst BUTTON_BASE =\n 'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50';\n\ninterface ApprovalCardProps {\n proposal: ToolApprovalProposal;\n decision: ToolApprovalDecision | undefined;\n onDecide: (decision: ToolApprovalDecision) => void;\n disabled?: boolean;\n t: (key: string) => string;\n}\n\n/**\n * One proposed call: what the agent wants to run, what it does, and every\n * argument labelled with the tool's own description of it.\n *\n * Those descriptions are the difference between a control and a rubber stamp.\n * `cascade: true` is unjudgeable; \"cascade — also delete linked entities\" is a\n * decision someone can actually make.\n */\nconst ApprovalCard = ({ proposal, decision, onDecide, disabled, t }: ApprovalCardProps) => {\n // Declining asks why before it sends. With argument editing deliberately\n // absent, a rejection IS the correction channel — it is the agent's only\n // signal to adapt. Optional, so a reviewer with nothing to add just confirms.\n const [rejecting, setRejecting] = useState(false);\n const [reason, setReason] = useState('');\n // Every proposal in a batch renders its own reason box, so the association\n // needs an id unique to this card rather than a constant.\n const reasonId = useId();\n const rejectPanelRef = useRevealIntoView<HTMLDivElement>(rejecting);\n\n const verdict = decision?.verdict;\n const argumentNames = Object.keys(proposal.arguments ?? {});\n\n const decide = (next: ToolApprovalVerdict) => {\n onDecide({\n toolCallId: proposal.toolCallId,\n verdict: next,\n ...(next === 'reject' && reason.trim() ? { rejectionReason: reason.trim() } : {}),\n });\n };\n\n return (\n <div className=\"rounded-lg border border-amber-500/25 bg-amber-500/[0.04] p-3 flex flex-col gap-2\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0 flex items-start gap-2\">\n <WrenchIcon size={13} className=\"mt-0.5 shrink-0 text-amber-600 dark:text-amber-400\" />\n <div className=\"min-w-0\">\n <p className=\"truncate font-mono text-xs text-gray-800 dark:text-white/85\">{proposal.toolName}</p>\n {proposal.toolDescription && <p className=\"mt-0.5 text-[0.7rem] text-gray-600 dark:text-white/60\">{proposal.toolDescription}</p>}\n {proposal.source && <p className=\"mt-0.5 text-[0.65rem] text-gray-400 dark:text-white/35\">{proposal.source}</p>}\n </div>\n </div>\n {verdict && (\n <span\n className={\n 'shrink-0 rounded px-1.5 py-0.5 text-[0.65rem] font-medium ' +\n (verdict === 'reject' ? 'bg-red-500/15 text-red-600 dark:text-red-300' : 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300')\n }\n >\n {verdict === 'reject' ? t('Declined') : verdict === 'approve_always' ? t('Always allowed') : t('Approved')}\n </span>\n )}\n </div>\n\n {argumentNames.length > 0 && (\n <div className=\"flex flex-col gap-1.5\">\n {argumentNames.map((name) => {\n const schema = argumentSchema(proposal.inputSchema, name);\n return (\n <div key={name} className=\"text-[0.7rem]\">\n <div className=\"flex flex-wrap items-baseline gap-1.5\">\n <span className=\"font-mono text-gray-700 dark:text-white/75\">{name}</span>\n {schema.description && <span className=\"text-gray-500 dark:text-white/45\">— {schema.description}</span>}\n </div>\n <pre className=\"mt-0.5 whitespace-pre-wrap break-all font-mono text-gray-600 dark:text-white/60\">\n {formatValue(proposal.arguments?.[name])}\n </pre>\n </div>\n );\n })}\n </div>\n )}\n\n {!verdict && rejecting && (\n <div ref={rejectPanelRef} className=\"flex flex-col gap-1.5\">\n <label htmlFor={reasonId} className=\"text-[0.7rem] text-gray-500 dark:text-white/45\">\n {t('Why not? The agent sees this and can adapt (optional)')}\n </label>\n <textarea\n id={reasonId}\n autoFocus\n rows={2}\n value={reason}\n onChange={(e) => setReason(e.target.value)}\n placeholder={t('e.g. wrong environment — use staging instead')}\n className=\"w-full resize-none rounded-md border border-gray-200 bg-white px-2 py-1 text-[0.7rem] text-gray-800 outline-none focus:border-[var(--chat-accent)]/50 dark:border-white/10 dark:bg-white/[0.03] dark:text-white/85\"\n />\n <div className=\"flex items-center gap-1.5\">\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => decide('reject')}\n className={`${BUTTON_BASE} bg-red-500/10 text-red-600 hover:bg-red-500/20 dark:text-red-300`}\n >\n <XCircleIcon size={13} />\n {t('Decline this call')}\n </button>\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => {\n setRejecting(false);\n setReason('');\n }}\n className={`${BUTTON_BASE} text-gray-500 hover:bg-gray-100 dark:text-white/50 dark:hover:bg-white/5`}\n >\n {t('Back')}\n </button>\n </div>\n </div>\n )}\n\n {!verdict && !rejecting && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => setRejecting(true)}\n className={`${BUTTON_BASE} text-red-600 hover:bg-red-500/10 dark:text-red-300`}\n >\n <XCircleIcon size={13} />\n {t('No')}\n </button>\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => decide('approve')}\n className={`${BUTTON_BASE} bg-[var(--chat-accent)] text-white hover:opacity-90`}\n >\n <CheckIcon size={13} />\n {t('Yes')}\n </button>\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => decide('approve_always')}\n /* Disclosed on the control itself rather than buried in a tooltip:\n this is the one verdict whose effect outlives the turn. */\n title={t('Also applies to your scheduled runs, until you revoke it')}\n className={`${BUTTON_BASE} border border-gray-200 text-gray-700 hover:bg-gray-100 dark:border-white/15 dark:text-white/70 dark:hover:bg-white/5`}\n >\n {t('Yes, always')}\n </button>\n </div>\n )}\n </div>\n );\n};\n\n/**\n * Collects a verdict for every call a paused turn is waiting on.\n *\n * Submission is all-or-nothing because the backend requires it: resuming with\n * an undecided call would leave its `tool_use` block without a `tool_result`,\n * which the model providers reject outright. That condition is met the moment\n * the last card is decided, so the set submits itself there — a verdict is\n * final once clicked (a decided card shows a badge, not buttons), so a separate\n * confirm would gate work already committed to.\n *\n * \"Yes, always\" is the exception and the only click that earns a confirm step:\n * it saves a standing preference for this user that applies to unattended runs\n * too, and that has to be read before it is committed.\n */\nexport const ChatApprovalPrompt = ({ proposals, onSubmit, isSubmitting, error, t }: ChatApprovalPromptProps) => {\n const [decisions, setDecisions] = useState<Record<string, ToolApprovalDecision>>({});\n // Guards the auto-submit against a double fire: React may re-render between\n // the last verdict and the parent flipping `isSubmitting`, and answering the\n // same pause twice would decide a turn that is already resuming.\n const submitted = useRef(false);\n // The same fact as state, because a ref cannot re-render the footer button.\n const [sent, setSent] = useState(false);\n\n // A failed submission is the one case where the prompt survives its own send:\n // the turn is still paused, so the controls have to come back for a retry.\n useEffect(() => {\n if (!error) return;\n submitted.current = false;\n setSent(false);\n }, [error]);\n\n const busy = isSubmitting || sent;\n\n const submit = (all: ToolApprovalDecision[]) => {\n if (submitted.current || isSubmitting) return;\n submitted.current = true;\n setSent(true);\n onSubmit(all);\n };\n\n const decidedCount = proposals.filter((p) => decisions[p.toolCallId]).length;\n const allDecided = decidedCount === proposals.length && proposals.length > 0;\n const willRunAlways = Object.values(decisions).some((d) => d.verdict === 'approve_always');\n\n // The warning and the confirm sit *below* the cards, so an \"always\" click can\n // push them past the fold — exactly what must be read before confirming.\n const footerRef = useRevealIntoView<HTMLDivElement>(willRunAlways);\n // The prompt arrives at the bottom of a thread that just grew by a whole\n // turn. Always true: the mount is the reveal.\n const rootRef = useRevealIntoView<HTMLDivElement>(true);\n\n return (\n <div ref={rootRef} className=\"flex flex-col gap-2 rounded-lg border border-amber-500/20 bg-amber-500/[0.02] p-3\">\n <p className=\"flex items-start gap-1.5 text-xs font-medium text-amber-700 dark:text-amber-300\">\n <AlertTriangleIcon size={13} className=\"mt-0.5 shrink-0\" />\n {proposals.length === 1 ? t('The agent needs your approval to run a tool:') : t('The agent needs your approval to run these tools:')}\n </p>\n\n {proposals.map((proposal) => (\n <ApprovalCard\n key={proposal.toolCallId}\n proposal={proposal}\n decision={decisions[proposal.toolCallId]}\n disabled={busy}\n t={t}\n onDecide={(decision) => {\n const next = { ...decisions, [decision.toolCallId]: decision };\n setDecisions(next);\n // Safe to read `decisions` from the closure: each verdict is its\n // own click, so this handler always sees the state the previous\n // one set.\n const all = proposals.map((p) => next[p.toolCallId]);\n if (all.every(Boolean) && !all.some((d) => d.verdict === 'approve_always')) {\n submit(all);\n }\n }}\n />\n ))}\n\n {willRunAlways && (\n <p className=\"flex items-start gap-1.5 text-[0.7rem] text-amber-700 dark:text-amber-300/90\">\n <AlertTriangleIcon size={13} className=\"mt-0.5 shrink-0\" />\n {t(\n '“Yes, always” saves a preference for you. That tool will then run without asking — including on scheduled runs nobody is watching — until you revoke it.',\n )}\n </p>\n )}\n\n {error && <p className=\"text-[0.7rem] text-red-600 dark:text-red-300\">{error}</p>}\n\n <div ref={footerRef} className=\"flex items-center justify-between gap-2\">\n {/* The counter earns its place only when there is more than one thing\n to count; on a single proposal it reads as a progress bar for a\n one-step process. */}\n <span className=\"text-[0.7rem] text-gray-500 dark:text-white/40\">\n {proposals.length > 1 ? `${decidedCount}/${proposals.length} ${t('decided')}` : ''}\n </span>\n <button\n type=\"button\"\n disabled={!allDecided || busy}\n onClick={() => submit(proposals.map((p) => decisions[p.toolCallId]))}\n className={`${BUTTON_BASE} bg-[var(--chat-accent)] text-white hover:opacity-90`}\n >\n {busy ? t('Sending…') : t('Confirm')}\n </button>\n </div>\n </div>\n );\n};\n","import { useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { CloseIcon, ImageIcon, MaximizeIcon } from './icons';\nimport { findChatbotRoot } from '../utils';\n\ninterface ImageLightboxProps {\n src: string;\n alt: string;\n onClose: () => void;\n t: (key: string) => string;\n}\n\n/**\n * Full-panel image viewer. Portalled into the chatbot root (not `document.body`)\n * so it inherits the panel's stacking context and can never be painted under —\n * or over — unrelated host chrome, matching the reasoning-details dialog.\n */\nconst ImageLightbox = ({ src, alt, onClose, t }: ImageLightboxProps) => {\n const hostRef = useRef<HTMLSpanElement>(null);\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n const [root, setRoot] = useState<HTMLElement | null>(null);\n\n useEffect(() => {\n setRoot(findChatbotRoot(hostRef.current));\n }, []);\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') onClose();\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [onClose]);\n\n // The overlay is the only interactive surface while open, so focus moves to\n // its close button and returns to the thumbnail on dismiss.\n useEffect(() => {\n if (!root) return;\n const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n closeButtonRef.current?.focus({ preventScroll: true });\n return () => previouslyFocused?.focus({ preventScroll: true });\n }, [root]);\n\n return (\n <span ref={hostRef} className=\"hidden\">\n {root &&\n createPortal(\n <div\n className=\"absolute inset-0 z-[10000] flex items-center justify-center bg-black/80 p-4\"\n onClick={onClose}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('Image preview')}\n >\n <button\n ref={closeButtonRef}\n type=\"button\"\n onClick={onClose}\n aria-label={t('Close')}\n className=\"absolute top-4 right-4 rounded-full bg-white/10 p-2 text-white transition-colors hover:bg-white/20\"\n >\n <CloseIcon size={20} />\n </button>\n <img src={src} alt={alt} className=\"max-h-[90%] max-w-[90%] rounded-lg object-contain\" onClick={(e) => e.stopPropagation()} />\n </div>,\n root,\n )}\n </span>\n );\n};\n\n/**\n * True for a URL with no scheme — a host-relative path such as\n * `/api/v1/platform/chat/files/<id>/download`. Protocol-relative URLs (`//host`)\n * are excluded: they resolve to a different origin, so the host's auth headers\n * must not be attached to them.\n */\nconst isRelativeUrl = (src: string): boolean => !src.startsWith('//') && !/^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(src);\n\ninterface ChatImageProps {\n src: string;\n alt: string;\n /**\n * Auth headers the host attaches to chatbot API calls. When present and the\n * image is host-relative, the image is fetched as a blob with these headers\n * instead of being handed to `<img src>` — a browser never sends custom\n * headers on an `<img>` request, so a bearer-authenticated image endpoint\n * would otherwise return 401 and render as a broken image.\n */\n requestHeaders?: Record<string, string>;\n /** Cap the inline thumbnail height. Full size is always available via the lightbox. */\n maxHeightClass?: string;\n t: (key: string) => string;\n}\n\n/**\n * An image inside a chat message: click (or the hover affordance) opens a\n * full-panel lightbox. Handles the three shapes an agent produces — inline\n * `data:image/*` charts from a code interpreter, host-relative API-served\n * images needing auth headers, and ordinary absolute URLs.\n */\nexport const ChatImage = ({ src, alt, requestHeaders, maxHeightClass = 'max-h-[400px]', t }: ChatImageProps) => {\n const needsAuth = !!requestHeaders && Object.keys(requestHeaders).length > 0 && isRelativeUrl(src);\n const [blobUrl, setBlobUrl] = useState<string | null>(null);\n const [errored, setErrored] = useState(false);\n const [expanded, setExpanded] = useState(false);\n\n useEffect(() => {\n if (!needsAuth) return;\n let cancelled = false;\n let objectUrl: string | null = null;\n\n fetch(src, { credentials: 'include', headers: { ...requestHeaders } })\n .then((res) => {\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n return res.blob();\n })\n .then((blob) => {\n if (cancelled) return;\n objectUrl = URL.createObjectURL(blob);\n setBlobUrl(objectUrl);\n })\n .catch(() => {\n if (!cancelled) setErrored(true);\n });\n\n return () => {\n cancelled = true;\n // Revoking on teardown (rather than on unmount only) keeps a src change\n // from leaking the previous blob.\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [needsAuth, src, requestHeaders]);\n\n const displaySrc = needsAuth ? blobUrl : src;\n\n if (errored) {\n return <p className=\"my-2 text-[0.75rem] italic text-gray-400 dark:text-white/40\">{t('Image could not be loaded')}</p>;\n }\n\n if (!displaySrc) {\n return (\n <span className=\"my-3 inline-flex items-center gap-2 rounded-lg border border-gray-200 px-3 py-2 dark:border-white/10\">\n <ImageIcon size={14} className=\"shrink-0 animate-pulse text-[var(--chat-accent)]\" />\n <span className=\"text-[0.7rem] text-gray-500 dark:text-white/40\">{t('Loading image…')}</span>\n </span>\n );\n }\n\n return (\n <>\n <span className=\"group/img relative my-3 inline-block max-w-full rounded-lg border border-gray-200 p-1 transition-colors hover:border-[var(--chat-accent)]/40 dark:border-white/10\">\n <img\n src={displaySrc}\n alt={alt}\n loading=\"lazy\"\n onClick={() => setExpanded(true)}\n onError={() => setErrored(true)}\n className={`max-w-full cursor-zoom-in rounded-md object-contain ${maxHeightClass}`}\n />\n <button\n type=\"button\"\n onClick={() => setExpanded(true)}\n aria-label={t('Expand image')}\n className=\"absolute top-2 right-2 rounded-md bg-black/50 p-1.5 text-white opacity-0 transition-opacity hover:bg-black/70 group-hover/img:opacity-100\"\n >\n <MaximizeIcon size={14} />\n </button>\n </span>\n {expanded && <ImageLightbox src={displaySrc} alt={alt} onClose={() => setExpanded(false)} t={t} />}\n </>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport { GamepadIcon } from './icons';\n\n/**\n * Playful, rotating \"still working on it\" messages shown below the status\n * bubble during longer waits. They double as the Space Invader's targets:\n * the little invader ship erases them letter by letter, then the next one\n * fades in. Kept short and upbeat so they fit on one line in the narrow\n * floating panel and read as a sense of progress rather than noise.\n */\nconst DEFAULT_MESSAGES = [\n 'Crunching the data',\n 'Connecting the dots',\n 'Consulting the sources',\n 'Thinking it through',\n 'Reticulating splines',\n 'Analyzing the details',\n 'Almost there',\n 'Putting it together',\n 'Polishing the answer',\n 'Wrapping things up',\n];\n\n/** localStorage key for the per-browser mini-game preference (on by default). */\nconst PREF_KEY = 'filigranChatMiniGame';\n\n/** Plain (no-game) message rotation cadence. */\nconst PLAIN_ROTATE_MS = 2600;\n\n/** Canvas height in CSS px — room for the target row + the gliding ship. */\nconst GAME_HEIGHT = 70;\n\n/** Chunky monospace for the retro arcade feel + even letter spacing. */\nconst arcadeFont = (px: number): string => `700 ${px}px 'Courier New', ui-monospace, monospace`;\n\n/**\n * Classic 11x8 \"crab\" invader, two leg frames toggled while it glides — the\n * silhouette reads instantly as a Space Invader. `1` = filled pixel.\n */\nconst INVADER_FRAMES: string[][] = [\n ['00100000100', '00010001000', '00111111100', '01101110110', '11111111111', '10111111101', '10100000101', '00011011000'],\n ['00100000100', '10010001001', '10111111101', '11101110111', '11111111111', '00111111100', '00100000100', '01000000010'],\n];\n\nfunction readPref(): boolean {\n if (typeof window === 'undefined') return true;\n try {\n return window.localStorage.getItem(PREF_KEY) !== 'off';\n } catch {\n return true;\n }\n}\n\nfunction writePref(on: boolean): void {\n try {\n window.localStorage.setItem(PREF_KEY, on ? 'on' : 'off');\n } catch {\n /* private mode / disabled storage — fall back to in-memory state only */\n }\n}\n\nfunction usePrefersReducedMotion(): boolean {\n const [reduced, setReduced] = useState(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n });\n useEffect(() => {\n if (typeof window === 'undefined' || !window.matchMedia) return;\n const mql = window.matchMedia('(prefers-reduced-motion: reduce)');\n const onChange = () => setReduced(mql.matches);\n // Older Safari/WebKit only expose the deprecated addListener/removeListener.\n if (typeof mql.addEventListener === 'function') {\n mql.addEventListener('change', onChange);\n return () => mql.removeEventListener('change', onChange);\n }\n mql.addListener(onChange);\n return () => mql.removeListener(onChange);\n }, []);\n return reduced;\n}\n\n/** Px-from-bottom within which the user is considered \"still following\". */\nconst FOLLOW_THRESHOLD_PX = 140;\n\n/** Nearest vertically-scrollable ancestor of `el`, or null. */\nfunction findScrollParent(el: HTMLElement | null): HTMLElement | null {\n let node = el?.parentElement ?? null;\n while (node) {\n const oy = getComputedStyle(node).overflowY;\n if ((oy === 'auto' || oy === 'scroll') && node.scrollHeight > node.clientHeight) {\n return node;\n }\n node = node.parentElement;\n }\n return null;\n}\n\ninterface Letter {\n char: string;\n x: number;\n w: number;\n alive: boolean;\n}\n\ninterface Bullet {\n x: number;\n y: number;\n}\n\ninterface Particle {\n x: number;\n y: number;\n vx: number;\n vy: number;\n life: number;\n}\n\n/**\n * Self-contained canvas mini-game. Owns its own rAF loop, resize handling and\n * lifecycle; the only thing it reports back is the index of the message it is\n * currently destroying, so the host can keep an accessible text mirror in sync.\n * Returns a teardown function.\n */\nfunction createInvaderGame(canvas: HTMLCanvasElement, messages: string[], onMessage: (index: number) => void): () => void {\n const maybeCtx = canvas.getContext('2d');\n if (!maybeCtx) return () => {};\n const ctx: CanvasRenderingContext2D = maybeCtx;\n\n let raf = 0;\n let cssW = 0;\n const cssH = GAME_HEIGHT;\n let fontPx = 13;\n let accent = '#7b5cff';\n\n let msgIndex = 0;\n let letters: Letter[] = [];\n let targetIndex = -1;\n let bullets: Bullet[] = [];\n let particles: Particle[] = [];\n let shipX = 0;\n let cooldown = 0;\n let clearedAt = 0;\n let legFrame = 0;\n let legTimer = 0;\n let last = performance.now();\n\n const PX = 2;\n const SPRITE_W = 11 * PX;\n const SPRITE_H = 8 * PX;\n const shipTop = cssH - SPRITE_H - 4;\n const letterY = Math.round(cssH * 0.42);\n\n function resolveAccent(): void {\n const v = getComputedStyle(canvas).getPropertyValue('--chat-accent').trim();\n if (v) accent = v;\n }\n\n function firstAlive(from: number): number {\n for (let i = from; i < letters.length; i++) {\n if (letters[i].alive) return i;\n }\n return -1;\n }\n\n function layout(): void {\n if (cssW <= 0) return;\n const text = messages[msgIndex % messages.length] || '';\n onMessage(msgIndex % messages.length);\n\n // Shrink the font until the message fits the available width.\n fontPx = 13;\n for (; fontPx >= 8; fontPx--) {\n ctx.font = arcadeFont(fontPx);\n if (ctx.measureText(text).width <= cssW - 16) break;\n }\n ctx.font = arcadeFont(fontPx);\n\n const widths = Array.from(text).map((ch) => ctx.measureText(ch).width);\n const total = widths.reduce((a, b) => a + b, 0);\n let x = (cssW - total) / 2;\n letters = Array.from(text).map((ch, i) => {\n const lx = x;\n x += widths[i];\n return { char: ch, x: lx, w: widths[i], alive: ch.trim().length > 0 };\n });\n targetIndex = firstAlive(0);\n bullets = [];\n particles = [];\n cooldown = 0;\n clearedAt = 0;\n if (shipX <= 0) shipX = cssW / 2;\n }\n\n function letterCenter(i: number): number {\n return letters[i].x + letters[i].w / 2;\n }\n\n function update(dt: number): void {\n const dtf = Math.min(dt / 16.6667, 3);\n\n legTimer += dt;\n if (legTimer > 320) {\n legTimer = 0;\n legFrame ^= 1;\n }\n\n if (targetIndex === -1) {\n // Message fully cleared — brief pause, then load the next one.\n if (clearedAt === 0) clearedAt = performance.now();\n else if (performance.now() - clearedAt > 650) {\n msgIndex++;\n layout();\n }\n } else {\n const targetX = letterCenter(targetIndex);\n shipX += (targetX - shipX) * Math.min(0.16 * dtf, 1);\n cooldown -= dt;\n if (bullets.length === 0 && cooldown <= 0 && Math.abs(shipX - targetX) < 4) {\n bullets.push({ x: shipX, y: shipTop });\n cooldown = 130;\n }\n }\n\n for (let i = bullets.length - 1; i >= 0; i--) {\n bullets[i].y -= 2.6 * dtf;\n if (bullets[i].y <= letterY) {\n // Hit: detonate the current target letter and advance.\n if (targetIndex !== -1) {\n const cx = letterCenter(targetIndex);\n letters[targetIndex].alive = false;\n for (let p = 0; p < 7; p++) {\n const ang = (Math.PI * 2 * p) / 7 + Math.random();\n const spd = 0.6 + Math.random() * 1.4;\n particles.push({ x: cx, y: letterY, vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd, life: 1 });\n }\n targetIndex = firstAlive(targetIndex + 1);\n }\n bullets.splice(i, 1);\n }\n }\n\n for (let i = particles.length - 1; i >= 0; i--) {\n const p = particles[i];\n p.x += p.vx * dtf;\n p.y += p.vy * dtf;\n p.life -= 0.045 * dtf;\n if (p.life <= 0) particles.splice(i, 1);\n }\n }\n\n function draw(): void {\n ctx.clearRect(0, 0, cssW, cssH);\n\n // Single solid fill colour throughout; vary opacity via globalAlpha rather\n // than CSS color-mix() strings, which are not reliably accepted as a canvas\n // fillStyle on every browser (some fall back to opaque black).\n ctx.fillStyle = accent;\n\n ctx.font = arcadeFont(fontPx);\n ctx.textBaseline = 'middle';\n ctx.globalAlpha = 0.85;\n for (const l of letters) {\n if (l.alive) ctx.fillText(l.char, l.x, letterY);\n }\n\n ctx.globalAlpha = 1;\n for (const b of bullets) {\n ctx.fillRect(b.x - 1, b.y, 2, 7);\n }\n\n for (const p of particles) {\n ctx.globalAlpha = Math.max(p.life, 0) * 0.9;\n ctx.fillRect(p.x - 1.5, p.y - 1.5, 3, 3);\n }\n\n ctx.globalAlpha = 1;\n const frame = INVADER_FRAMES[legFrame];\n const ox = Math.round(shipX - SPRITE_W / 2);\n for (let r = 0; r < frame.length; r++) {\n const row = frame[r];\n for (let c = 0; c < row.length; c++) {\n if (row[c] === '1') ctx.fillRect(ox + c * PX, shipTop + r * PX, PX, PX);\n }\n }\n }\n\n function loop(now: number): void {\n const dt = now - last;\n last = now;\n if (!document.hidden && cssW > 0) {\n update(dt);\n draw();\n }\n raf = requestAnimationFrame(loop);\n }\n\n function applySize(): void {\n const rect = canvas.getBoundingClientRect();\n const dpr = window.devicePixelRatio || 1;\n cssW = rect.width;\n canvas.width = Math.max(1, Math.round(cssW * dpr));\n canvas.height = Math.round(cssH * dpr);\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n resolveAccent();\n layout();\n }\n\n // Prefer ResizeObserver; fall back to a window resize listener on older\n // browsers / embedded webviews where it is unavailable (an unguarded\n // `new ResizeObserver(...)` would throw and break the waiting experience).\n let ro: ResizeObserver | null = null;\n if (typeof ResizeObserver !== 'undefined') {\n ro = new ResizeObserver(applySize);\n ro.observe(canvas);\n } else {\n window.addEventListener('resize', applySize);\n }\n applySize();\n raf = requestAnimationFrame(loop);\n\n return () => {\n cancelAnimationFrame(raf);\n if (ro) ro.disconnect();\n else window.removeEventListener('resize', applySize);\n };\n}\n\ninterface ChatWaitingGameProps {\n t: (key: string) => string;\n /** Host-level override; when false the feature is hidden entirely. */\n enabled?: boolean;\n}\n\n/**\n * Waiting experience shown below the status bubble during longer waits:\n * dynamic rotating messages with an optional Space Invader mini-game that\n * shoots the message letters away one by one. The game can be toggled off\n * per browser (preference persisted in localStorage); when off — or when the\n * OS requests reduced motion — the messages simply rotate as plain dimmed\n * text, so the \"dynamic loading messages\" feedback always stands on its own.\n */\nexport const ChatWaitingGame = ({ t, enabled = true }: ChatWaitingGameProps) => {\n const messages = useMemo(() => DEFAULT_MESSAGES.map((m) => t(m)), [t]);\n const reducedMotion = usePrefersReducedMotion();\n const [minigameOn, setMinigameOn] = useState(readPref);\n const [msgIndex, setMsgIndex] = useState(0);\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const rootRef = useRef<HTMLDivElement>(null);\n\n const playMode = enabled && minigameOn && !reducedMotion;\n\n // The game mounts below the last message but, unlike streamed reasoning/answer\n // text (which auto-scrolls on length change), nothing else triggers a scroll —\n // so at the bottom it lands under the fold. Reveal it on mount IF the user is\n // still following the bottom; if they scrolled up to read history, leave them.\n useEffect(() => {\n const scroller = findScrollParent(rootRef.current);\n if (!scroller) return;\n const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;\n if (distance <= FOLLOW_THRESHOLD_PX) {\n scroller.scrollTop = scroller.scrollHeight;\n }\n }, []);\n\n // Plain-text rotation when the game is off / reduced motion. Skipped while the\n // game drives the index, and entirely when the feature is disabled (so a\n // disabled host arms no timer); `enabled` is a dep so toggling it cleans up.\n useEffect(() => {\n if (playMode || !enabled) return;\n const id = window.setInterval(() => setMsgIndex((i) => (i + 1) % messages.length), PLAIN_ROTATE_MS);\n return () => window.clearInterval(id);\n }, [playMode, enabled, messages.length]);\n\n // Canvas engine when the game is on.\n useEffect(() => {\n if (!playMode) return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n return createInvaderGame(canvas, messages, setMsgIndex);\n }, [playMode, messages]);\n\n if (!enabled) return null;\n\n const current = messages[msgIndex % messages.length];\n\n return (\n <div ref={rootRef} className=\"ml-11 mt-2.5 max-w-[78%]\">\n {/* Keep the live region scoped to the announced text only — wrapping the\n whole UI (including the toggle button) made screen readers re-announce\n the control alongside each message change. */}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {current}\n </span>\n {/* Suppress every non-essential animation (fade-ins, the blinking caret)\n under prefers-reduced-motion, so the reduced-motion path really is\n motion-free — only the plain dimmed text rotates. */}\n <div\n className=\"relative overflow-hidden rounded-md bg-[var(--chat-accent)]/[0.03]\"\n style={reducedMotion ? undefined : { animation: 'chat-fade-in 0.5s ease-out' }}\n >\n {playMode ? (\n <canvas ref={canvasRef} aria-hidden className=\"block w-full\" style={{ height: GAME_HEIGHT }} />\n ) : (\n <div className=\"flex items-center\" style={{ height: GAME_HEIGHT }}>\n <span\n key={current}\n className=\"px-3 text-xs text-gray-500 dark:text-white/45\"\n style={reducedMotion ? undefined : { animation: 'chat-fade-in 0.4s ease-out' }}\n >\n {current}\n <span className={`ml-0.5 inline-block w-1 h-3 align-middle bg-[var(--chat-accent)]/60 ${reducedMotion ? '' : 'animate-pulse'}`} />\n </span>\n </div>\n )}\n {!reducedMotion && (\n <button\n type=\"button\"\n onClick={() => {\n const next = !minigameOn;\n setMinigameOn(next);\n writePref(next);\n }}\n aria-pressed={minigameOn}\n aria-label={minigameOn ? t('Turn off the waiting mini-game') : t('Turn on the waiting mini-game')}\n title={minigameOn ? t('Turn off the waiting mini-game') : t('Turn on the waiting mini-game')}\n className={`absolute top-1 right-1 rounded p-1 transition-opacity ${\n minigameOn ? 'text-[var(--chat-accent)] opacity-50 hover:opacity-100' : 'text-gray-400 dark:text-white/40 opacity-40 hover:opacity-90'\n }`}\n >\n <GamepadIcon size={13} />\n </button>\n )}\n </div>\n </div>\n );\n};\n","import { useEffect, useRef, useState } from 'react';\nimport type { AgentStatusState, IconProps } from '../types';\nimport {\n AlertTriangleIcon,\n BrainIcon,\n DatabaseIcon,\n ExternalLinkIcon,\n GlobeIcon,\n MailIcon,\n SearchIcon,\n SparklesIcon,\n TerminalIcon,\n UserPlusIcon,\n WrenchIcon,\n} from './icons';\nimport { ChatWaitingGame } from './ChatWaitingGame';\n\ninterface ChatThinkingProps {\n agentStatus: AgentStatusState | null;\n logoIcon?: React.ReactNode;\n t: (key: string) => string;\n /** Host-level override for the waiting mini-game / dynamic messages. */\n miniGameEnabled?: boolean;\n}\n\ntype IconComponent = (props: IconProps) => React.JSX.Element;\n\ninterface StatusVisual {\n label: string;\n StatusIcon: IconComponent;\n showDots: boolean;\n}\n\nfunction resolveStatusVisual(agentStatus: AgentStatusState | null, t: (key: string) => string): StatusVisual {\n if (!agentStatus) {\n return { label: t('Thinking...'), StatusIcon: BrainIcon, showDots: false };\n }\n switch (agentStatus.status) {\n case 'tool_start': {\n const rawNames = agentStatus.tools ?? [];\n const lower = rawNames.map((n) => n.toLowerCase());\n\n // Delegation tools have dedicated statuses\n if (lower.some((n) => n === 'spawn_background_task')) {\n const count = rawNames.filter((n) => n === 'spawn_background_task').length;\n const label = count > 1 ? `${t('Delegating')} ${count} ${t('tasks')}…` : `${t('Delegating task')}…`;\n return { label, StatusIcon: UserPlusIcon, showDots: false };\n }\n if (lower.some((n) => n === 'check_task_status')) {\n const count = rawNames.filter((n) => n === 'check_task_status').length;\n const target = count > 1 ? `${count} ${t('background tasks')}` : t('background task');\n return { label: `${t('Waiting for')} ${target}…`, StatusIcon: SparklesIcon, showDots: false };\n }\n if (lower.some((n) => n === 'get_task_result')) {\n const count = rawNames.filter((n) => n === 'get_task_result').length;\n const from = count > 1 ? `${count} ${t('tasks')}` : t('task');\n return { label: `${t('Collecting results from')} ${from}…`, StatusIcon: SparklesIcon, showDots: false };\n }\n\n let StatusIcon: IconComponent = WrenchIcon;\n if (lower.some((n) => n.includes('search') || n.includes('list'))) {\n StatusIcon = SearchIcon;\n } else if (lower.some((n) => n.includes('read') || n.includes('get') || n.includes('query'))) {\n StatusIcon = DatabaseIcon;\n } else if (lower.some((n) => n.includes('send') || n.includes('create') || n.includes('draft') || n.includes('reply') || n.includes('flag'))) {\n StatusIcon = MailIcon;\n } else if (lower.some((n) => n.includes('code') || n.includes('execute'))) {\n StatusIcon = TerminalIcon;\n } else if (lower.some((n) => n.includes('web') || n.includes('browse'))) {\n StatusIcon = GlobeIcon;\n }\n let label: string;\n if (rawNames.length > 0) {\n const display = rawNames.map((n) => n.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase()));\n const unique = Array.from(new Set(display));\n label = unique.length === 1 ? `${unique[0]}…` : `${unique[0]} (+${unique.length - 1} more)…`;\n } else {\n label = t('Using tools…');\n }\n return { label, StatusIcon, showDots: false };\n }\n case 'awaiting_approval':\n // Not progress: the turn has stopped and the stream is held open while a\n // person decides. Shown only when the prompt itself is not rendered (a\n // host wiring `ChatMessages` without a decision handler) — otherwise the\n // prompt replaces this bubble entirely.\n return { label: t('Waiting for your approval…'), StatusIcon: AlertTriangleIcon, showDots: false };\n case 'analyzing':\n return { label: t('Analyzing results…'), StatusIcon: SparklesIcon, showDots: false };\n case 'steering':\n return { label: t('Incorporating your message…'), StatusIcon: SparklesIcon, showDots: true };\n case 'composing':\n return { label: t('Composing answer…'), StatusIcon: BrainIcon, showDots: true };\n case 'consulting': {\n const consultName = agentStatus.tools?.[0] ?? 'agent';\n return { label: `${t('Consulting')} ${consultName}…`, StatusIcon: UserPlusIcon, showDots: false };\n }\n case 'delegating': {\n const count = agentStatus.tools?.filter((n) => n === 'spawn_background_task').length ?? 0;\n return {\n label: count > 1 ? `${t('Delegating')} ${count} ${t('tasks')}…` : `${t('Delegating task')}…`,\n StatusIcon: UserPlusIcon,\n showDots: false,\n };\n }\n case 'polling': {\n const checkCount = agentStatus.tools?.filter((n) => n === 'check_task_status').length ?? 0;\n const target = checkCount > 1 ? `${checkCount} ${t('background tasks')}` : t('background task');\n return { label: `${t('Waiting for')} ${target}…`, StatusIcon: SparklesIcon, showDots: false };\n }\n case 'collecting': {\n const fetchCount = agentStatus.tools?.filter((n) => n === 'get_task_result').length ?? 0;\n const from = fetchCount > 1 ? `${fetchCount} ${t('tasks')}` : t('task');\n return { label: `${t('Collecting results from')} ${from}…`, StatusIcon: SparklesIcon, showDots: false };\n }\n case 'transferring': {\n const targetName = agentStatus.tools?.[0] ?? 'agent';\n return { label: `${t('Transferring to')} ${targetName}…`, StatusIcon: ExternalLinkIcon, showDots: false };\n }\n case 'thinking':\n default:\n return { label: t('Thinking...'), StatusIcon: BrainIcon, showDots: false };\n }\n}\n\n/**\n * Light markdown cleanup for reasoning prose. Preserves paragraph breaks so\n * multi-step reasoning stays readable inside the small scrolling window\n * instead of collapsing into one unbroken blob.\n */\nexport function cleanReasoningText(text: string): string {\n return text\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/`([^`]+)`/g, '$1')\n .replace(/\\*\\*(.+?)\\*\\*/g, '$1')\n .replace(/__(.+?)__/g, '$1')\n .replace(/#{1,6}\\s+/g, '')\n .replace(/^[ \\t]*[-*>]+[ \\t]*/gm, '')\n .replace(/[ \\t]+/g, ' ')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n/**\n * Reasoning window — the model's reasoning prose rendered below the status\n * bubble while the agent works: smaller, dimmed text inside a capped-height\n * (max-h-40) window always pinned to the newest line, with a Cursor-style\n * top dissolve once full (soft gradient fade at the top only — the text reads\n * as scrolling up and dissolving; the bottom stays sharp) — framed by the\n * breathing accent left-border glow. The window is intentionally NOT\n * user-scrollable (overflow-hidden, no scrollbar): the prose is ambient\n * feedback that scrolls up and dissolves; the full accumulated reasoning\n * stays readable afterwards via the message's reasoning details. The window\n * (and the status bubble above it) disappears the moment the final answer\n * starts flowing.\n */\nexport function ThinkingTextBubble({ content }: { content: string }) {\n const ref = useRef<HTMLDivElement>(null);\n const [isOverflowing, setIsOverflowing] = useState(false);\n const cleaned = cleanReasoningText(content);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n el.scrollTop = el.scrollHeight;\n setIsOverflowing(el.scrollHeight > el.clientHeight + 1);\n }, [cleaned]);\n\n if (cleaned.length < 3) return null;\n\n return (\n <div\n className=\"ml-11 mt-2.5 max-w-[75%] rounded-md border-l-2 bg-[var(--chat-accent)]/[0.03] py-2 pl-3 pr-3\"\n style={{ animation: 'reasoningGlow 3s ease-in-out infinite, chat-fade-in 0.5s ease-out' }}\n >\n <div\n ref={ref}\n className={`max-h-40 overflow-hidden${\n isOverflowing\n ? // -webkit- twin first: Safari/WebKit ignores unprefixed mask-image\n // on older versions, which would silently drop the top dissolve.\n ' [-webkit-mask-image:linear-gradient(to_bottom,transparent_0,rgb(0_0_0/0.25)_1.5rem,rgb(0_0_0/0.7)_3rem,black_4.5rem)]' +\n ' [mask-image:linear-gradient(to_bottom,transparent_0,rgb(0_0_0/0.25)_1.5rem,rgb(0_0_0/0.7)_3rem,black_4.5rem)]'\n : ''\n }`}\n >\n <p className=\"m-0 whitespace-pre-wrap break-words text-xs leading-5 text-gray-500 dark:text-white/45\">{cleaned}</p>\n </div>\n </div>\n );\n}\n\n/** Render seconds as a compact elapsed label (e.g. `45s`, `3m 20s`). */\nfunction formatElapsed(seconds: number): string {\n // Floor at the boundary: `elapsed_s` comes from the backend and may be a\n // float, which would otherwise render as \"45.3s\" / \"3m 20.5s\".\n const total = Math.floor(seconds);\n if (total < 60) return `${total}s`;\n const m = Math.floor(total / 60);\n const s = total % 60;\n return s > 0 ? `${m}m ${s}s` : `${m}m`;\n}\n\n/**\n * Elapsed time is only surfaced once the current operation has been\n * running long enough that the user could wonder whether it is stuck.\n */\nconst ELAPSED_DISPLAY_THRESHOLD_S = 15;\n\n/**\n * The reasoning window flips to the waiting game once nothing has progressed\n * for this long — i.e. no reasoning at all, or a reasoning stream that stalled.\n */\nconst STALL_DELAY_MS = 5000;\n\n/**\n * True once `signal` has stayed unchanged for `delayMs`. Re-arms whenever the\n * signal changes, so resumed reasoning clears the flag immediately. Used to\n * detect a stalled (or absent) reasoning stream so we can show the waiting game\n * in the meantime and flip back to the reasoning the moment it resumes. Arms no\n * timer (and never flips) while `enabled` is false, so a host that disables the\n * waiting game schedules no timeouts or re-renders for it.\n */\nfunction useStalled(signal: number, delayMs: number, enabled: boolean): boolean {\n const [stalled, setStalled] = useState(false);\n const prevSignalRef = useRef(signal);\n\n // Did the signal change since the last settled render? When it did, reasoning\n // has just resumed, so we report \"not stalled\" for this very render without a\n // render-phase state update (discouraged in React / brittle under StrictMode\n // and concurrent rendering). The effect below then resets the flag and re-arms\n // the timer — deriving the value here keeps the flip back to the reasoning\n // window free of the one-frame lag a clear-in-effect alone would leave.\n const signalChanged = prevSignalRef.current !== signal;\n\n useEffect(() => {\n prevSignalRef.current = signal;\n setStalled(false);\n if (!enabled) return;\n const id = window.setTimeout(() => setStalled(true), delayMs);\n return () => window.clearTimeout(id);\n }, [signal, delayMs, enabled]);\n\n return enabled && stalled && !signalChanged;\n}\n\nexport const ChatThinking = ({ agentStatus, logoIcon, t, miniGameEnabled = true }: ChatThinkingProps) => {\n const { label, StatusIcon, showDots } = resolveStatusVisual(agentStatus, t);\n const thinkingContent = agentStatus?.thinkingContent;\n\n // Tick the elapsed counter every second. The backend only reports a fresh\n // value once every ~10s (`tool_heartbeat`), so rendering that integer\n // directly makes the timer sit still and then jump ~10s at a time. Each\n // heartbeat re-anchors `elapsedStartMs` (the instant elapsed was zero); the\n // displayed seconds are derived from that anchor plus a local 1s ticker, so\n // the number moves smoothly and self-corrects on every beat. The interval\n // only runs while an anchor is present, and this state is local to the\n // indicator so the tick never re-renders the surrounding message list.\n const elapsedStartMs = agentStatus?.elapsedStartMs;\n const [nowMs, setNowMs] = useState(() => Date.now());\n useEffect(() => {\n if (elapsedStartMs == null) return;\n setNowMs(Date.now());\n const id = window.setInterval(() => setNowMs(Date.now()), 1000);\n return () => window.clearInterval(id);\n }, [elapsedStartMs]);\n\n // Fall back to the raw heartbeat value for backends/protocols that report\n // `elapsedS` without an anchor.\n const elapsedS = elapsedStartMs != null ? Math.max(0, (nowMs - elapsedStartMs) / 1000) : agentStatus?.elapsedS;\n const showElapsed = typeof elapsedS === 'number' && elapsedS >= ELAPSED_DISPLAY_THRESHOLD_S;\n // Show the waiting game when reasoning is absent or has stalled for 5s; flip\n // back to the reasoning window the moment new reasoning text resumes (the\n // accumulated content carries the continuation).\n const stalled = useStalled(thinkingContent?.length ?? 0, STALL_DELAY_MS, miniGameEnabled);\n const showGame = miniGameEnabled && stalled;\n\n return (\n <>\n <div className=\"flex gap-3 items-center justify-start\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[var(--chat-accent)]/15 to-[var(--chat-accent)]/5\">\n <span className=\"text-[var(--chat-accent)] [&>svg]:w-4 [&>svg]:h-4\">{logoIcon}</span>\n </div>\n <div className=\"rounded-lg bg-gray-50 dark:bg-white/[0.03] px-4 py-3 relative overflow-hidden\">\n <div className=\"absolute inset-0 bg-gradient-to-r from-[var(--chat-accent)]/[0.03] via-transparent to-[var(--chat-accent)]/[0.03] animate-pulse pointer-events-none\" />\n <div className=\"relative flex items-center gap-2.5\">\n {showDots ? (\n <div className=\"flex gap-[3px] items-center h-3.5 w-3.5 justify-center\">\n {[0, 0.15, 0.3].map((delay, i) => (\n <span\n key={i}\n className=\"h-[5px] w-[5px] rounded-full bg-[var(--chat-accent)]/50\"\n style={{ animation: `chat-dot 1s ease-in-out infinite ${delay}s` }}\n />\n ))}\n </div>\n ) : (\n <StatusIcon size={14} className=\"text-[var(--chat-accent)] animate-pulse transition-all duration-300\" />\n )}\n <span className=\"text-sm text-gray-500 dark:text-white/50 transition-all duration-300\">{label}</span>\n {showElapsed && <span className=\"text-xs text-gray-400 dark:text-white/30 tabular-nums shrink-0\">{formatElapsed(elapsedS)}</span>}\n </div>\n </div>\n </div>\n {thinkingContent && !showGame ? (\n <ThinkingTextBubble content={thinkingContent} />\n ) : showGame ? (\n <ChatWaitingGame t={t} enabled={miniGameEnabled} />\n ) : null}\n </>\n );\n};\n","import { memo, useMemo, useState } from 'react';\nimport Markdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport remarkBreaks from 'remark-breaks';\nimport { CheckIcon, CopyIcon } from './icons';\nimport { ChatImage } from './ChatImage';\nimport { hardenNestedCodeFences, identity, markdownUrlTransform, normalizeImageMarkdown, normalizeMarkdownTables, wrapBareJson } from '../utils';\n\ninterface MarkdownMessageProps {\n content: string;\n onRelativeLinkClick?: (href: string) => void;\n /** Auth headers used to fetch host-relative images (see `ChatImage`). */\n requestHeaders?: Record<string, string>;\n t?: (key: string) => string;\n}\n\nconst isRelativeHref = (href?: string) => {\n if (!href) return false;\n if (href.startsWith('//')) return false;\n const hasAbsoluteScheme = /^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(href);\n return !hasAbsoluteScheme;\n};\n\n/**\n * Resolve an href to its host-app-internal form, or null when it points\n * elsewhere.\n *\n * Internal links must route through the host application's router\n * (`onRelativeLinkClick`) instead of a full page load / new tab. Two shapes\n * qualify:\n *\n * 1. Relative hrefs (`/dashboard/...`) — kept as-is.\n * 2. Absolute http(s) hrefs on the SAME origin as the embedding page\n * (e.g. `https://octi.example.com/dashboard/id/<uuid>`) — reduced to\n * `pathname + search + hash`. Backends intentionally emit absolute links\n * (so links work from any chat surface); when the chatbot is embedded in\n * that very platform the link must still navigate in-app.\n *\n * Anything else (other origins, non-http schemes, malformed URLs) returns\n * null and falls back to a regular new-tab anchor.\n */\nconst toInternalHref = (href?: string): string | null => {\n if (!href) return null;\n if (isRelativeHref(href)) return href;\n if (typeof window === 'undefined') return null;\n try {\n const url = new URL(href, window.location.href);\n if ((url.protocol === 'http:' || url.protocol === 'https:') && url.origin === window.location.origin) {\n return `${url.pathname}${url.search}${url.hash}` || '/';\n }\n } catch {\n /* malformed URL — treat as external */\n }\n return null;\n};\n\n/**\n * Assistant prose renderer.\n *\n * Memoized because the message list re-renders on every streamed frame: without\n * it, each settled message in the thread would re-run the full remark parse on\n * every frame, which is the dominant source of streaming jank in long threads.\n * With it, only the live bubble re-parses.\n */\nexport const MarkdownMessage = memo(({ content, onRelativeLinkClick, requestHeaders, t = identity }: MarkdownMessageProps) => {\n const [copiedBlock, setCopiedBlock] = useState<string | null>(null);\n\n // Preprocess once per `content`: this component re-renders on UI-only state\n // (e.g. `copiedBlock`), and every pass scans the whole message, so memoizing\n // keeps that work off the hot path for large messages. Order matters —\n // image alt-text is flattened before anything else looks at line structure,\n // and the JSON wrap must see the raw payload before fences are hardened.\n const processedContent = useMemo(\n () => hardenNestedCodeFences(normalizeMarkdownTables(wrapBareJson(normalizeImageMarkdown(content)))),\n [content],\n );\n\n const handleCopyCode = (code: string) => {\n navigator.clipboard.writeText(code);\n setCopiedBlock(code);\n setTimeout(() => setCopiedBlock(null), 2000);\n };\n\n return (\n <Markdown\n remarkPlugins={[remarkGfm, remarkBreaks]}\n urlTransform={markdownUrlTransform}\n components={{\n p: ({ children }) => <p className=\"mb-3 last:mb-0 leading-7 break-words text-[0.8125rem] text-gray-900 dark:text-white/90\">{children}</p>,\n // A fenced block renders as `<pre><code>`; the `code` override below\n // returns a `<div>` wrapper, which is not valid inside `<pre>`. Passing\n // the children straight through keeps the markup well-formed.\n pre: ({ children }) => <>{children}</>,\n code: ({ className, children }) => {\n const match = /language-(\\w+)/.exec(className || '');\n const codeStr = String(children).replace(/\\n$/, '');\n // A fence with no info string (```\\n…\\n```) carries no `language-*`\n // class. Falling back to the multi-line test keeps it a block instead\n // of collapsing it into a single run of inline code.\n if (match || codeStr.includes('\\n')) {\n return (\n <div className=\"my-3 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden bg-gray-50 dark:bg-white/[0.03]\">\n <div className=\"flex items-center justify-between px-3 py-1.5 border-b border-gray-200 dark:border-white/10 bg-gray-100 dark:bg-white/[0.03]\">\n <span className=\"text-[0.7rem] text-gray-500 dark:text-white/40 font-mono\">{match?.[1] ?? 'plaintext'}</span>\n <button\n type=\"button\"\n onClick={() => handleCopyCode(codeStr)}\n aria-label={copiedBlock === codeStr ? t('Copied') : t('Copy code')}\n className=\"p-0.5 rounded-sm hover:bg-gray-200 dark:hover:bg-white/10 transition-colors\"\n >\n {copiedBlock === codeStr ? (\n <CheckIcon size={14} className=\"text-green-500\" />\n ) : (\n <CopyIcon size={14} className=\"text-gray-400 dark:text-white/40\" />\n )}\n </button>\n </div>\n <pre className=\"m-0 px-3 py-2 overflow-x-auto\">\n <code className=\"font-mono text-xs leading-[1.7] text-gray-800 dark:text-white/90 whitespace-pre\">{codeStr}</code>\n </pre>\n </div>\n );\n }\n return (\n <code className=\"bg-gray-100 dark:bg-white/[0.08] px-1.5 py-0.5 rounded-sm font-mono text-xs text-[var(--chat-accent)]\">{children}</code>\n );\n },\n // `list-disc` / `list-decimal` are required, not decorative: the\n // package's scoped preflight resets `list-style` inside the panel, so\n // without them every list renders as unmarked, indented prose.\n ul: ({ children }) => (\n <ul className=\"list-disc pl-5 mb-3 text-[0.8125rem] text-gray-900 dark:text-white/90 [&_li]:mb-1 marker:text-[var(--chat-accent)]/50\">\n {children}\n </ul>\n ),\n ol: ({ children }) => (\n <ol className=\"list-decimal pl-5 mb-3 text-[0.8125rem] text-gray-900 dark:text-white/90 [&_li]:mb-1 marker:text-[var(--chat-accent)]/50\">\n {children}\n </ol>\n ),\n li: ({ children }) => <li className=\"leading-7 break-words\">{children}</li>,\n strong: ({ children }) => <strong className=\"font-semibold text-gray-900 dark:text-white\">{children}</strong>,\n em: ({ children }) => <em className=\"italic\">{children}</em>,\n hr: () => <hr className=\"my-4 border-gray-200 dark:border-white/10\" />,\n blockquote: ({ children }) => (\n <blockquote className=\"my-3 border-l-2 border-[var(--chat-accent)]/30 bg-[var(--chat-accent)]/[0.03] pl-4 pr-3 py-2 rounded-r-md italic text-gray-500 dark:text-white/60\">\n {children}\n </blockquote>\n ),\n a: ({ href, children }) => {\n const internalHref = toInternalHref(href);\n const routeInternally = internalHref !== null && !!onRelativeLinkClick;\n const openInNewTab = !routeInternally && !isRelativeHref(href);\n const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {\n if (!routeInternally) return;\n event.preventDefault();\n onRelativeLinkClick!(internalHref!);\n };\n\n return (\n <a\n href={href}\n onClick={handleClick}\n target={openInNewTab ? '_blank' : undefined}\n rel={openInNewTab ? 'noopener noreferrer' : undefined}\n className=\"text-[var(--chat-accent)] underline underline-offset-2 hover:brightness-125\"\n >\n {children}\n </a>\n );\n },\n img: ({ src, alt }) => {\n if (typeof src !== 'string' || !src) return null;\n return <ChatImage src={src} alt={alt || ''} requestHeaders={requestHeaders} t={t} />;\n },\n h1: ({ children }) => <h1 className=\"mt-4 first:mt-0 mb-2 font-bold text-base text-gray-900 dark:text-white\">{children}</h1>,\n h2: ({ children }) => <h2 className=\"mt-3 first:mt-0 mb-2 font-bold text-[0.9rem] text-gray-900 dark:text-white\">{children}</h2>,\n h3: ({ children }) => <h3 className=\"mt-3 first:mt-0 mb-1.5 font-semibold text-[0.85rem] text-gray-900 dark:text-white\">{children}</h3>,\n table: ({ children }) => (\n <div className=\"my-3 overflow-x-auto rounded-lg border border-gray-200 dark:border-white/10\">\n <table className=\"w-full border-collapse text-xs\">{children}</table>\n </div>\n ),\n tr: ({ children }) => <tr className=\"transition-colors hover:bg-gray-50 dark:hover:bg-white/[0.02]\">{children}</tr>,\n th: ({ children }) => (\n <th className=\"px-3 py-2 text-left font-semibold bg-gray-50 dark:bg-white/[0.04] border-b border-gray-200 dark:border-white/10 text-gray-900 dark:text-white\">\n {children}\n </th>\n ),\n td: ({ children }) => (\n <td className=\"px-3 py-2 border-b border-gray-200 dark:border-white/10 text-gray-700 dark:text-white/80\">{children}</td>\n ),\n }}\n >\n {processedContent}\n </Markdown>\n );\n});\n\nMarkdownMessage.displayName = 'MarkdownMessage';\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport type { ChatMessage, ToolCallTraceEntry } from '../types';\nimport {\n AlertTriangleIcon,\n ArrowRightLeftIcon,\n BotIcon,\n BrainIcon,\n CheckCircleIcon,\n ChevronDownIcon,\n CloseIcon,\n WrenchIcon,\n XCircleIcon,\n} from './icons';\nimport { cleanReasoningText } from './ChatThinking';\nimport { findChatbotRoot } from '../utils';\n\n/** Trace values longer than this are shown raw instead of pretty-printed JSON. */\nconst TRACE_PRETTY_LIMIT = 10_000;\n\n/**\n * Pretty-print a tool-call input/output when it is compact JSON; anything\n * else (plain text, oversized payloads, malformed JSON) is shown raw.\n */\nfunction prettyTraceValue(raw: string | undefined): string {\n if (!raw) return '';\n if (raw.length > TRACE_PRETTY_LIMIT) return raw;\n try {\n return JSON.stringify(JSON.parse(raw), null, 2);\n } catch {\n return raw;\n }\n}\n\nfunction toolDisplayName(name: string): string {\n return name.replace(/_/g, ' ');\n}\n\n/** Expandable row for a single tool call in the reasoning-details dialog. */\nconst ToolCallRow = ({ entry, index, t }: { entry: ToolCallTraceEntry; index: number; t: (key: string) => string }) => {\n const [expanded, setExpanded] = useState(false);\n\n const inputDisplay = useMemo(() => prettyTraceValue(entry.input), [entry.input]);\n const outputDisplay = useMemo(() => prettyTraceValue(entry.output), [entry.output]);\n const hasInput = !!inputDisplay && inputDisplay !== '{}';\n\n return (\n <div className=\"border border-gray-200 dark:border-white/[0.06] rounded-md overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-expanded={expanded}\n className=\"w-full flex items-center gap-2.5 px-3 py-2 hover:bg-gray-50 dark:hover:bg-white/[0.03] transition-colors text-left\"\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-[var(--chat-accent)]/10 text-[0.65rem] font-medium text-[var(--chat-accent)]\">\n {index + 1}\n </span>\n {entry.success ? (\n <CheckCircleIcon size={12} className=\"shrink-0 text-emerald-500 dark:text-emerald-400\" />\n ) : (\n <XCircleIcon size={12} className=\"shrink-0 text-red-500 dark:text-red-400\" />\n )}\n <span className=\"flex-1 min-w-0 text-[0.8125rem] text-gray-700 dark:text-white/80 truncate font-mono\">{toolDisplayName(entry.name)}</span>\n <ChevronDownIcon\n size={14}\n className={`shrink-0 text-gray-400 dark:text-white/50 transition-transform duration-200 ${expanded ? 'rotate-180' : ''}`}\n />\n </button>\n\n {expanded && (\n <div className=\"border-t border-gray-200 dark:border-white/[0.06]\">\n {hasInput && (\n <div className=\"px-3 py-2 border-b border-gray-100 dark:border-white/[0.04] bg-gray-50/50 dark:bg-white/[0.01]\">\n <p className=\"m-0 mb-1.5 text-[0.6rem] text-gray-500 dark:text-white/40 uppercase tracking-wider font-medium\">{t('Input')}</p>\n <pre className=\"m-0 text-[0.7rem] text-gray-600 dark:text-white/60 font-mono whitespace-pre-wrap break-all leading-relaxed max-h-40 overflow-y-auto filigran-chat-scrollable\">\n {inputDisplay}\n </pre>\n </div>\n )}\n <div className=\"px-3 py-2 bg-gray-50/50 dark:bg-white/[0.01]\">\n <p className=\"m-0 mb-1.5 text-[0.6rem] text-gray-500 dark:text-white/40 uppercase tracking-wider font-medium\">{t('Output')}</p>\n <pre className=\"m-0 text-[0.7rem] text-gray-600 dark:text-white/60 font-mono whitespace-pre-wrap break-all leading-relaxed max-h-48 overflow-y-auto filigran-chat-scrollable\">\n {outputDisplay || t('(no output)')}\n </pre>\n </div>\n </div>\n )}\n </div>\n );\n};\n\ninterface ReasoningDetailsDialogProps {\n msg: ChatMessage;\n onClose: () => void;\n t: (key: string) => string;\n}\n\n/**\n * Modal dialog with the full reasoning details of an assistant message —\n * mirrors the XTM One web chat dialog (truncation warning, model reasoning,\n * expandable per-tool-call trace, transfer chain). Rendered as an overlay\n * covering the chatbot panel so it works in every mode and host without\n * depending on the host app's stacking order.\n */\nexport const ReasoningDetailsDialog = ({ msg, onClose, t }: ReasoningDetailsDialogProps) => {\n const hostRef = useRef<HTMLSpanElement>(null);\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n const dialogRef = useRef<HTMLDivElement>(null);\n const [root, setRoot] = useState<HTMLElement | null>(null);\n\n useEffect(() => {\n setRoot(findChatbotRoot(hostRef.current));\n }, []);\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n onClose();\n return;\n }\n // Focus trap: aria-modal promises focus stays inside the dialog, so\n // Tab/Shift+Tab cycle within it instead of escaping to the panel.\n if (e.key !== 'Tab') return;\n const dialog = dialogRef.current;\n if (!dialog) return;\n const focusable = dialog.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');\n if (focusable.length === 0) return;\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n if (e.shiftKey) {\n if (active === first || !dialog.contains(active)) {\n e.preventDefault();\n last.focus();\n }\n } else if (active === last || !dialog.contains(active)) {\n e.preventDefault();\n first.focus();\n }\n };\n document.addEventListener('keydown', onKeyDown);\n return () => document.removeEventListener('keydown', onKeyDown);\n }, [onClose]);\n\n // Move initial keyboard focus into the modal (aria-modal) once the portal\n // is mounted, and hand it back to the trigger when the dialog closes.\n useEffect(() => {\n if (!root) return;\n const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n closeButtonRef.current?.focus({ preventScroll: true });\n return () => previouslyFocused?.focus({ preventScroll: true });\n }, [root]);\n\n // Prefer the backend's explicit count, then the detailed trace (what the\n // dialog body actually renders), then the flat tool-name list — keeps the\n // header summary consistent with the rows below.\n const totalCalls = msg.toolCallCount ?? msg.toolCallTrace?.length ?? msg.toolNames?.length ?? 0;\n const tools = msg.toolNames ?? [];\n const iterations = msg.iterations ?? 1;\n const transfers = msg.transferChain ?? [];\n const trace = msg.toolCallTrace ?? [];\n const reasoning = (msg.reasoning ?? '').trim();\n\n const summaryParts = [\n iterations > 1 ? `${iterations} ${t('iterations')}` : '',\n `${totalCalls} ${totalCalls === 1 ? t('tool call') : t('tool calls')}`,\n transfers.length > 0 ? `${transfers.length} ${transfers.length === 1 ? t('transfer') : t('transfers')}` : '',\n ].filter(Boolean);\n\n return (\n <span ref={hostRef} className=\"hidden\">\n {root &&\n createPortal(\n <div\n className=\"absolute inset-0 z-[10000] flex items-center justify-center bg-black/30 dark:bg-black/50 p-4\"\n onClick={onClose}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={t('Reasoning details')}\n onClick={(e) => e.stopPropagation()}\n className=\"w-full max-w-md max-h-full flex flex-col rounded-xl border border-gray-200 dark:border-white/10 bg-white dark:bg-[#1e1e2e] shadow-[0_8px_32px_rgba(0,0,0,0.25)] dark:shadow-[0_8px_32px_rgba(0,0,0,0.6)]\"\n >\n <div className=\"px-4 pt-3.5 pb-2.5 border-b border-gray-200 dark:border-white/10\">\n <div className=\"flex items-center gap-2\">\n <WrenchIcon size={15} className=\"text-[var(--chat-accent)]\" />\n <span className=\"flex-1 text-[0.875rem] font-semibold text-gray-900 dark:text-white\">{t('Reasoning details')}</span>\n <button\n ref={closeButtonRef}\n type=\"button\"\n onClick={onClose}\n aria-label={t('Close')}\n className=\"w-7 h-7 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/10 text-gray-500 dark:text-white/40 hover:text-gray-700 dark:hover:text-white/70 transition-colors\"\n >\n <CloseIcon size={16} />\n </button>\n </div>\n <p className=\"m-0 mt-0.5 text-[0.72rem] text-gray-500 dark:text-white/40\">{summaryParts.join(' · ')}</p>\n </div>\n\n <div className=\"px-4 py-3 overflow-y-auto filigran-chat-scrollable flex flex-col gap-3\">\n {msg.isTruncated && (\n <div className=\"flex items-start gap-2.5 rounded-md border border-amber-500/20 bg-amber-500/5 px-3 py-2.5 text-[0.72rem] text-amber-600 dark:text-amber-300/90\">\n <AlertTriangleIcon size={14} className=\"shrink-0 mt-0.5 text-amber-500 dark:text-amber-400\" />\n <span>\n <span className=\"font-semibold\">{t('Turn limit reached.')}</span>{' '}\n {t(\n \"The agent's iteration budget was exhausted - execution stopped before completing all planned steps. The final response is a best-effort summary of work done so far.\",\n )}\n </span>\n </div>\n )}\n\n {reasoning && (\n <div>\n <div className=\"flex items-center gap-1.5 mb-1.5\">\n <BrainIcon size={13} className=\"text-[var(--chat-accent)]/70\" />\n <span className=\"text-[0.72rem] font-medium text-gray-500 dark:text-white/50\">{t('Model reasoning')}</span>\n </div>\n <div className=\"rounded-md border border-gray-200 dark:border-white/[0.06] bg-gray-50/50 dark:bg-white/[0.01] px-2.5 py-2 max-h-44 overflow-y-auto filigran-chat-scrollable\">\n <p className=\"m-0 whitespace-pre-wrap break-words text-[0.72rem] leading-5 text-gray-500 dark:text-white/45\">\n {cleanReasoningText(reasoning)}\n </p>\n </div>\n </div>\n )}\n\n {trace.length > 0 ? (\n <div className=\"flex flex-col gap-1.5\">\n {trace.map((entry, i) => (\n <ToolCallRow key={`${entry.name}-${i}`} entry={entry} index={i} t={t} />\n ))}\n </div>\n ) : (\n tools.length > 0 && (\n // Fallback when no detailed trace is available (legacy\n // messages / backends without trace support).\n <div>\n {tools.map((tn, i) => (\n <div\n key={`${tn}-${i}`}\n className=\"flex items-center gap-3 py-2 border-b border-gray-100 dark:border-white/[0.04] last:border-0\"\n >\n <span className=\"flex h-5 w-5 shrink-0 items-center justify-center rounded bg-[var(--chat-accent)]/10 text-[0.65rem] font-medium text-[var(--chat-accent)]\">\n {i + 1}\n </span>\n <WrenchIcon size={12} className=\"shrink-0 text-gray-400 dark:text-white/50\" />\n <span className=\"text-[0.8125rem] text-gray-700 dark:text-white/80 truncate font-mono\">{toolDisplayName(tn)}</span>\n </div>\n ))}\n </div>\n )\n )}\n\n {transfers.length > 0 && (\n <div>\n <div className=\"flex items-center gap-1.5 mb-1.5\">\n <ArrowRightLeftIcon size={13} className=\"text-[var(--chat-accent)]/70\" />\n <span className=\"text-[0.72rem] font-medium text-gray-500 dark:text-white/50\">{t('Transfer chain')}</span>\n </div>\n <div className=\"flex items-center gap-1.5 flex-wrap\">\n {transfers.map((tr, i) => (\n // Composite key: the same agent can appear twice in a\n // chain (A -> B -> A) and older payloads have no id.\n <div key={`${tr.agentId}-${i}`} className=\"flex items-center gap-1.5\">\n {i > 0 && <span className=\"text-gray-300 dark:text-white/30 text-[0.72rem]\">→</span>}\n <span className=\"inline-flex items-center gap-1 rounded-md bg-[var(--chat-accent)]/10 px-2 py-0.5 text-[0.72rem] font-medium text-[var(--chat-accent)]\">\n <BotIcon size={12} />\n {tr.agentName}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n </div>\n </div>,\n root,\n )}\n </span>\n );\n};\n","import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type {\n AgentStatusState,\n ChatAttachment,\n ChatMessage,\n MessageFeedback,\n ToolApprovalDecision,\n ToolApprovalProposal,\n} from '../types';\nimport { splitFileMarkers, stripFileMarkers } from '../utils';\nimport { AlertTriangleIcon, CheckIcon, ChevronDownIcon, CopyIcon, DownloadIcon, FileIcon, InfoIcon, ThumbsDownIcon, ThumbsUpIcon } from './icons';\nimport { ChatApprovalPrompt } from './ChatApprovalPrompt';\nimport { ChatImage } from './ChatImage';\nimport { ChatThinking } from './ChatThinking';\nimport { MarkdownMessage } from './MarkdownMessage';\nimport { ReasoningDetailsDialog } from './ReasoningDetailsDialog';\n\n/**\n * Windowed thread rendering: only the most recent slice of the thread is\n * mounted, and \"Load earlier messages\" walks the window back a step at a time.\n * A long restored conversation would otherwise mount hundreds of markdown\n * subtrees at once, which stalls the panel on open and on every streamed frame.\n */\nconst INITIAL_RENDER_WINDOW = 150;\nconst RENDER_WINDOW_STEP = 50;\n\n/** How long the copy affordance stays in its confirmed state. */\nconst COPY_FEEDBACK_DELAY = 2000;\n\ninterface ChatMessagesProps {\n messages: ChatMessage[];\n isLoading: boolean;\n agentStatus: AgentStatusState | null;\n agentName: string;\n logoIcon: React.ReactNode;\n onRelativeLinkClick?: (href: string) => void;\n /** Download an agent-generated file via the host app's backend proxy. */\n onDownloadFile?: (attachment: ChatAttachment) => void;\n /**\n * Resolve the host-proxied URL of an attachment, used to preview image\n * attachments inline. Attachments stay chips/cards when omitted.\n */\n resolveAttachmentUrl?: (attachment: ChatAttachment) => string | undefined;\n /** Auth headers used when fetching previewed images (see `ChatImage`). */\n requestHeaders?: Record<string, string>;\n /** Host-level override for the waiting mini-game / dynamic messages. */\n miniGameEnabled?: boolean;\n /** Enables the 👍/👎 affordance on completed assistant messages. */\n onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;\n /**\n * True while a turn answered after a reload is finishing without a stream.\n * Rendered as the ordinary working indicator: from the user's side nothing\n * about it is unusual, and a decision that visibly leads nowhere reads as a\n * broken button.\n */\n isResumingAfterDecision?: boolean;\n /**\n * Tool calls the running turn has paused on, awaiting a decision. Rendered\n * below the transcript, since the pause belongs to the turn rather than to\n * any one bubble.\n */\n pendingApprovals?: ToolApprovalProposal[] | null;\n onSubmitApprovalDecisions?: (decisions: ToolApprovalDecision[]) => void;\n isSubmittingApproval?: boolean;\n approvalError?: string | null;\n t: (key: string) => string;\n}\n\nfunction formatFileSize(bytes?: number): string {\n if (!bytes || bytes <= 0) return '';\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n/** Short uppercase extension label for a file chip (e.g. `report.pdf` → `PDF`). */\nfunction fileExtensionLabel(filename: string): string | undefined {\n const dot = filename.lastIndexOf('.');\n if (dot <= 0 || dot === filename.length - 1) return undefined;\n const ext = filename.slice(dot + 1);\n return ext.length <= 8 ? ext.toUpperCase() : undefined;\n}\n\nconst IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'avif']);\n\n/**\n * True when an attachment is worth previewing inline rather than showing as a\n * download card. The MIME type is authoritative; the short `type` label and the\n * filename extension are fallbacks for backends that omit it.\n */\nfunction isImageAttachment(att: ChatAttachment): boolean {\n if (att.contentType?.toLowerCase().startsWith('image/')) return true;\n const label = att.type?.toLowerCase();\n if (label && IMAGE_EXTENSIONS.has(label)) return true;\n const dot = att.filename.lastIndexOf('.');\n return dot > 0 && IMAGE_EXTENSIONS.has(att.filename.slice(dot + 1).toLowerCase());\n}\n\n/** Copies the assistant's answer as plain text. Revealed on message hover. */\nconst MessageCopyButton = ({ text, t }: { text: string; t: (key: string) => string }) => {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(text || '');\n setCopied(true);\n setTimeout(() => setCopied(false), COPY_FEEDBACK_DELAY);\n } catch {\n /* Clipboard unavailable (insecure context / denied permission) — the\n button simply doesn't confirm rather than throwing at the user. */\n }\n };\n\n return (\n <button\n type=\"button\"\n onClick={handleCopy}\n title={copied ? t('Copied!') : t('Copy response')}\n aria-label={copied ? t('Copied!') : t('Copy response')}\n className={`p-1 rounded-lg transition-opacity ${\n copied\n ? 'opacity-100 text-green-500 dark:text-green-400'\n : 'opacity-0 group-hover/msg:opacity-100 focus-visible:opacity-100 hover:text-[var(--chat-accent)] focus-visible:text-[var(--chat-accent)]'\n }`}\n >\n {copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}\n </button>\n );\n};\n\n/** 👍/👎 on a completed answer. Clicking the active value clears it. */\nconst MessageFeedbackButtons = ({\n value,\n onChange,\n t,\n}: {\n value: MessageFeedback | null;\n onChange: (next: MessageFeedback | null) => void;\n t: (key: string) => string;\n}) => {\n // Same focus-visible treatment as the copy button: these are hover-revealed,\n // so without it a keyboard user tabs onto an invisible control.\n const buttonClass = (active: boolean) =>\n `p-1 rounded-lg transition-opacity ${\n active\n ? 'opacity-100 text-[var(--chat-accent)]'\n : 'opacity-0 group-hover/msg:opacity-100 focus-visible:opacity-100 hover:text-[var(--chat-accent)] focus-visible:text-[var(--chat-accent)]'\n }`;\n\n return (\n <>\n <button\n type=\"button\"\n onClick={() => onChange(value === 'up' ? null : 'up')}\n title={t('Good response')}\n aria-label={t('Good response')}\n aria-pressed={value === 'up'}\n className={buttonClass(value === 'up')}\n >\n <ThumbsUpIcon size={14} filled={value === 'up'} />\n </button>\n <button\n type=\"button\"\n onClick={() => onChange(value === 'down' ? null : 'down')}\n title={t('Bad response')}\n aria-label={t('Bad response')}\n aria-pressed={value === 'down'}\n className={buttonClass(value === 'down')}\n >\n <ThumbsDownIcon size={14} filled={value === 'down'} />\n </button>\n </>\n );\n};\n\ninterface MessageRowProps {\n msg: ChatMessage;\n /** True only for the message currently being streamed. */\n isStreaming: boolean;\n agentName: string;\n logoIcon: React.ReactNode;\n onRelativeLinkClick?: (href: string) => void;\n onDownloadFile?: (attachment: ChatAttachment) => void;\n resolveAttachmentUrl?: (attachment: ChatAttachment) => string | undefined;\n requestHeaders?: Record<string, string>;\n feedback: MessageFeedback | null;\n onFeedbackChange?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;\n t: (key: string) => string;\n}\n\n/**\n * One message in the thread.\n *\n * Memoized: the parent re-renders on every streamed frame, and without this\n * every settled message would re-run its markdown parse and re-mount its\n * attachment cards on each frame.\n */\nconst MessageRow = memo(\n ({\n msg,\n isStreaming,\n agentName,\n logoIcon,\n onRelativeLinkClick,\n onDownloadFile,\n resolveAttachmentUrl,\n requestHeaders,\n feedback,\n onFeedbackChange,\n t,\n }: MessageRowProps) => {\n const [showReasoning, setShowReasoning] = useState(false);\n const isAssistant = msg.role === 'assistant';\n const isEmpty = !msg.content;\n\n const renderAttachmentCard = (att: ChatAttachment, key: string) => {\n // An image the host can resolve a URL for is shown, not filed away.\n const previewUrl = resolveAttachmentUrl && isImageAttachment(att) ? resolveAttachmentUrl(att) : undefined;\n if (previewUrl) {\n return <ChatImage key={key} src={previewUrl} alt={att.filename} requestHeaders={requestHeaders} maxHeightClass=\"max-h-[200px]\" t={t} />;\n }\n\n const isWorking = att.fileTag === 'working_file';\n const sizeLabel = formatFileSize(att.size);\n return (\n <button\n key={key}\n type=\"button\"\n onClick={() => onDownloadFile?.(att)}\n title={t('Download')}\n className={`group flex items-center gap-2 text-left rounded-lg border px-2.5 py-1.5 transition-colors cursor-pointer max-w-[90%] ${\n isWorking\n ? 'border-gray-200 dark:border-white/10 bg-transparent'\n : 'border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/[0.04] hover:border-[var(--chat-accent)] hover:bg-[var(--chat-accent)]/5'\n }`}\n >\n <span className={`shrink-0 ${isWorking ? 'text-gray-400 dark:text-white/40' : 'text-[var(--chat-accent)]'}`}>\n <FileIcon size={16} />\n </span>\n <span className=\"flex flex-col min-w-0 flex-1\">\n <span className=\"truncate text-[0.75rem] text-gray-900 dark:text-white\">{att.filename}</span>\n {(att.type || sizeLabel) && (\n <span className=\"text-[0.65rem] text-gray-400 dark:text-white/40 uppercase\">{[att.type, sizeLabel].filter(Boolean).join(' · ')}</span>\n )}\n </span>\n <span className=\"shrink-0 text-gray-400 dark:text-white/30 group-hover:text-[var(--chat-accent)]\">\n <DownloadIcon size={15} />\n </span>\n </button>\n );\n };\n\n // A user-uploaded file shown as a non-clickable chip — used while the upload\n // is still in flight (no server `fileId` yet) or when no download handler is\n // wired by the host.\n const renderFileChip = (name: string, key: string) => (\n <span\n key={key}\n className=\"inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-gray-200 dark:border-white/10 text-[0.7rem] text-gray-600 dark:text-white/60\"\n >\n <FileIcon size={14} />\n {name}\n </span>\n );\n\n // Render assistant content as an ordered interleave of prose segments and\n // download cards, so a reply with markers like\n // `text [[FILE:a]] more text [[FILE:b]]` keeps the cards at their source\n // position. Cards only render when a download handler is wired\n // (`onDownloadFile`); attachments whose marker isn't found in the prose are\n // appended as a fallback. During streaming the attachments aren't hydrated\n // yet, so only prose (markers stripped) renders.\n const buildAssistantBlocks = (): React.ReactNode[] => {\n const parts = splitFileMarkers(msg.content);\n const attByFileId = new Map((msg.attachments ?? []).map((a) => [a.fileId, a] as const));\n const used = new Set<string>();\n const blocks: React.ReactNode[] = [];\n\n parts.forEach((part, i) => {\n if (part.type === 'text') {\n if (part.value.trim()) {\n blocks.push(\n <div key={`t-${i}`} className=\"max-w-[90%] pl-1 py-1 text-[0.8125rem] leading-7\">\n <MarkdownMessage content={part.value} onRelativeLinkClick={onRelativeLinkClick} requestHeaders={requestHeaders} t={t} />\n </div>,\n );\n }\n } else if (onDownloadFile) {\n const att = attByFileId.get(part.fileId);\n if (att) {\n used.add(part.fileId);\n blocks.push(renderAttachmentCard(att, `f-${part.fileId}-${i}`));\n }\n }\n });\n\n if (onDownloadFile) {\n (msg.attachments ?? []).forEach((att) => {\n if (!used.has(att.fileId)) {\n blocks.push(renderAttachmentCard(att, `orphan-${att.fileId}`));\n }\n });\n }\n\n // An assistant reply that is *only* a file marker leaves no prose; show a\n // subtle ellipsis (not an empty padded bubble) when nothing else rendered\n // and we're not still streaming.\n if (blocks.length === 0 && !isStreaming) {\n blocks.push(\n <span key=\"empty\" className=\"pl-1 text-[0.8125rem] text-gray-400 dark:text-white/40 italic\">\n ...\n </span>,\n );\n }\n\n return blocks;\n };\n\n // Build the file cards shown on a user message. A successfully-uploaded file\n // carries a server `fileId`, so it renders as the same download card as an\n // agent-generated attachment (re-using the host download proxy via\n // `onDownloadFile`) — uploaded files must stay downloadable, not just\n // displayed. Files still uploading (no `fileId` / not `done`) or hosts\n // without a download handler fall back to a static chip. On conversation\n // restore the backend re-surfaces user uploads as `attachments` (there are\n // no live `files`), so those are rendered too.\n const buildUserFileBlocks = (): React.ReactNode[] => {\n const blocks: React.ReactNode[] = [];\n const seen = new Set<string>();\n\n (msg.files ?? []).forEach((f, i) => {\n const downloadable = !!(onDownloadFile && f.fileId && f.uploadStatus === 'done');\n if (downloadable && f.fileId) {\n seen.add(f.fileId);\n blocks.push(\n renderAttachmentCard(\n { fileId: f.fileId, filename: f.name, type: fileExtensionLabel(f.name), size: f.size, contentType: f.type },\n `file-${f.fileId}-${i}`,\n ),\n );\n } else {\n blocks.push(renderFileChip(f.name, `file-${i}`));\n }\n });\n\n (msg.attachments ?? []).forEach((att, i) => {\n if (seen.has(att.fileId)) return;\n seen.add(att.fileId);\n blocks.push(onDownloadFile ? renderAttachmentCard(att, `att-${att.fileId}-${i}`) : renderFileChip(att.filename, `att-${i}`));\n });\n\n return blocks;\n };\n\n const hasReasoningDetails =\n (msg.toolNames && msg.toolNames.length > 0) ||\n !!(msg.reasoning ?? '').trim() ||\n (msg.toolCallTrace && msg.toolCallTrace.length > 0) ||\n (msg.transferChain && msg.transferChain.length > 0) ||\n msg.isTruncated;\n\n // Actions only make sense on a finished answer, so they stay hidden while\n // the message is still streaming.\n const showActions = isAssistant && !isEmpty && !isStreaming;\n\n return (\n <div className={`group/msg flex flex-col ${isAssistant ? 'items-start' : 'items-end'}`}>\n {isAssistant && (\n <div className=\"flex items-center gap-1.5 mb-1\">\n <div className=\"w-6 h-6 rounded-lg flex items-center justify-center bg-gradient-to-br from-[var(--chat-accent)]/20 to-[var(--chat-accent)]/5\">\n <span className=\"text-[var(--chat-accent)] [&>svg]:w-3 [&>svg]:h-3\">{logoIcon}</span>\n </div>\n {/*\n The message's own agent wins over the panel-wide one. That name is\n whoever is selected right now, which is simply wrong for history —\n a thread that changed hands mid-way must keep each answer under\n the agent that wrote it.\n */}\n <span className=\"font-semibold text-xs text-gray-900 dark:text-white\">{msg.agentName ?? agentName}</span>\n </div>\n )}\n\n {!isAssistant && ((msg.files?.length ?? 0) > 0 || (msg.attachments?.length ?? 0) > 0) && (\n <div className=\"flex gap-1.5 flex-wrap mb-1.5 justify-end\">{buildUserFileBlocks()}</div>\n )}\n\n {isAssistant ? (\n <div className=\"flex flex-col gap-1.5 w-full items-start\">\n {buildAssistantBlocks()}\n {!isEmpty && isStreaming && <span className=\"inline-block w-1.5 h-4 bg-[var(--chat-accent)]/70 rounded-xs ml-1 animate-pulse\" />}\n </div>\n ) : (\n <div className=\"max-w-[90%] px-3.5 py-2 rounded-[14px_14px_4px_14px] bg-[var(--chat-accent-dark)] text-white text-[0.8125rem] leading-6\">\n {msg.content}\n </div>\n )}\n\n {showActions && (\n <div className=\"mt-0.5 flex items-center gap-0.5 text-gray-400 dark:text-white/40\">\n <MessageCopyButton text={stripFileMarkers(msg.content)} t={t} />\n {onFeedbackChange && (\n <MessageFeedbackButtons value={feedback} onChange={(next) => onFeedbackChange(msg.id, next, msg)} t={t} />\n )}\n {hasReasoningDetails && (\n <button\n type=\"button\"\n onClick={() => setShowReasoning((v) => !v)}\n className={`p-1 rounded-lg transition-opacity ${\n msg.isTruncated\n ? // A truncated turn must be visible at a glance (not gated\n // on hover) so the user notices the warning — mirrors the\n // XTM One web chat affordance.\n 'opacity-100 text-amber-500 dark:text-amber-400 hover:text-amber-600 dark:hover:text-amber-300'\n : 'opacity-50 hover:opacity-100 hover:text-[var(--chat-accent)]'\n }`}\n title={msg.isTruncated ? t('Reasoning details — turn limit reached') : t('Reasoning details')}\n aria-label={msg.isTruncated ? t('Reasoning details — turn limit reached') : t('Reasoning details')}\n aria-haspopup=\"dialog\"\n aria-expanded={showReasoning}\n >\n {msg.isTruncated ? <AlertTriangleIcon size={14} /> : <InfoIcon size={14} />}\n </button>\n )}\n </div>\n )}\n {showReasoning && <ReasoningDetailsDialog msg={msg} onClose={() => setShowReasoning(false)} t={t} />}\n </div>\n );\n },\n);\n\nMessageRow.displayName = 'MessageRow';\n\nexport const ChatMessages = ({\n messages,\n isLoading,\n agentStatus,\n agentName,\n logoIcon,\n onRelativeLinkClick,\n onDownloadFile,\n resolveAttachmentUrl,\n requestHeaders,\n miniGameEnabled = true,\n onMessageFeedback,\n isResumingAfterDecision,\n pendingApprovals,\n onSubmitApprovalDecisions,\n isSubmittingApproval,\n approvalError,\n t,\n}: ChatMessagesProps) => {\n const messagesEndRef = useRef<HTMLDivElement>(null);\n const [renderWindow, setRenderWindow] = useState(INITIAL_RENDER_WINDOW);\n const [feedbackByMessage, setFeedbackByMessage] = useState<Record<string, MessageFeedback>>({});\n\n useEffect(() => {\n messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, [messages]);\n\n // Keep the bottom in view while the reasoning window below the status\n // bubble grows: thinking prose streams in without any `messages` change,\n // so without this the growing window slides under the fold and the user\n // stops seeing the live reasoning. `behavior: 'instant'` (CSSOM View,\n // Baseline-supported) forces a non-animated jump — this fires on every\n // reasoning chunk and smooth animations would queue up; 'auto' would not\n // do, since a `scroll-behavior: smooth` ancestor turns it smooth again.\n const thinkingLen = agentStatus?.thinkingContent?.length ?? 0;\n useEffect(() => {\n if (!thinkingLen) return;\n messagesEndRef.current?.scrollIntoView({ behavior: 'instant' });\n }, [thinkingLen]);\n\n // Switching conversation (restore / new chat) replaces the whole array, so\n // the window must snap back to the tail instead of keeping a widened one.\n const firstMessageId = messages[0]?.id;\n useEffect(() => {\n setRenderWindow(INITIAL_RENDER_WINDOW);\n }, [firstMessageId]);\n\n const hasEarlierMessages = messages.length > renderWindow;\n const visibleMessages = useMemo(\n () => (hasEarlierMessages ? messages.slice(messages.length - renderWindow) : messages),\n [messages, renderWindow, hasEarlierMessages],\n );\n\n const handleFeedbackChange = useCallback(\n (messageId: string, next: MessageFeedback | null, message: ChatMessage) => {\n setFeedbackByMessage((prev) => {\n if (next !== null) return { ...prev, [messageId]: next };\n if (!(messageId in prev)) return prev;\n const rest = { ...prev };\n delete rest[messageId];\n return rest;\n });\n onMessageFeedback?.(messageId, next, message);\n },\n [onMessageFeedback],\n );\n\n // The streaming response is the LAST ASSISTANT message — not necessarily\n // the last message overall: a mid-run steering send appends an optimistic\n // user bubble after the assistant message that is still streaming. Gating\n // on `messages.length - 1` would then drop the live cursor / ChatThinking\n // state the moment the user steers.\n let streamingMessageId: string | null = null;\n if (isLoading) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === 'assistant') {\n streamingMessageId = messages[i].id;\n break;\n }\n }\n }\n\n // Only a prompt something can actually answer counts: without a submit\n // handler the controls would collect verdicts with nowhere to send them.\n const awaitingApproval = !!pendingApprovals?.length && !!onSubmitApprovalDecisions;\n\n return (\n <div className=\"flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-4 filigran-chat-scrollable\">\n {hasEarlierMessages && (\n <div className=\"flex justify-center\">\n <button\n type=\"button\"\n onClick={() => setRenderWindow((w) => w + RENDER_WINDOW_STEP)}\n className=\"flex items-center gap-1.5 rounded-full border border-gray-200 dark:border-white/10 px-3 py-1 text-[0.7rem] text-gray-500 dark:text-white/50 transition-colors hover:border-[var(--chat-accent)]/40 hover:text-[var(--chat-accent)]\"\n >\n <ChevronDownIcon size={13} className=\"rotate-180\" />\n {t('Load earlier messages')}\n </button>\n </div>\n )}\n\n {visibleMessages.map((msg) => {\n const isStreamingMessage = msg.id === streamingMessageId;\n // An assistant message with no content yet is the \"agent is working\"\n // placeholder, replaced by the live status bubble.\n if (msg.role === 'assistant' && !msg.content && isStreamingMessage) {\n // A turn paused for approval is not working, it is waiting on the\n // person reading it — so the progress bubble (and the waiting game\n // it grows into) gives way to the prompt rendered below.\n if (awaitingApproval) return null;\n return (\n <div key={msg.id}>\n <ChatThinking agentStatus={agentStatus} logoIcon={logoIcon} t={t} miniGameEnabled={miniGameEnabled} />\n </div>\n );\n }\n\n return (\n <MessageRow\n key={msg.id}\n msg={msg}\n isStreaming={isStreamingMessage}\n agentName={agentName}\n logoIcon={logoIcon}\n onRelativeLinkClick={onRelativeLinkClick}\n onDownloadFile={onDownloadFile}\n resolveAttachmentUrl={resolveAttachmentUrl}\n requestHeaders={requestHeaders}\n feedback={feedbackByMessage[msg.id] ?? null}\n onFeedbackChange={onMessageFeedback ? handleFeedbackChange : undefined}\n t={t}\n />\n );\n })}\n {/* Not tied to a placeholder message like the streaming indicator: a\n restore replaces the whole transcript on every poll, so there is no\n bubble of ours left to hang it on. */}\n {isResumingAfterDecision && !awaitingApproval && (\n <ChatThinking agentStatus={agentStatus} logoIcon={logoIcon} t={t} miniGameEnabled={miniGameEnabled} />\n )}\n {awaitingApproval && (\n <ChatApprovalPrompt\n // A second pause in the same turn is a new question, not a continuation\n // of the answered one: remounting drops the verdicts and the\n // already-submitted guard the previous set left behind.\n key={pendingApprovals!.map((p) => p.toolCallId).join('|')}\n proposals={pendingApprovals!}\n onSubmit={onSubmitApprovalDecisions!}\n isSubmitting={isSubmittingApproval}\n error={approvalError}\n t={t}\n />\n )}\n <div ref={messagesEndRef} />\n </div>\n );\n};\n","interface ChatWelcomeProps {\n firstName: string;\n logoIcon: React.ReactNode;\n promptSuggestions: string[];\n onPromptClick: (prompt: string) => void;\n /** Selected agent, so the screen says who is about to answer. */\n agentName?: string;\n agentDescription?: string | null;\n /** True while agent-specific suggestions are being fetched. */\n suggestionsLoading?: boolean;\n t: (key: string) => string;\n}\n\nexport const ChatWelcome = ({\n firstName,\n logoIcon,\n promptSuggestions,\n onPromptClick,\n agentName,\n agentDescription,\n suggestionsLoading = false,\n t,\n}: ChatWelcomeProps) => (\n <div className=\"flex-1 flex flex-col items-center justify-center px-6 pb-8\">\n <span className=\"text-[var(--chat-accent)] mb-4 [&>svg]:w-12 [&>svg]:h-12 drop-shadow-[0_0_12px_var(--chat-accent-40)]\">{logoIcon}</span>\n\n {/*\n Naming the agent here is the only confirmation that switching in the\n header actually took effect: the thread resets to this screen, so without\n it nothing tells you which agent the next message will reach.\n `key` on the heading restarts the fade whenever the agent changes, so the\n switch is visible rather than a silent text swap.\n */}\n <h2\n key={agentName ?? 'default'}\n className=\"text-xl font-medium mb-1 text-center text-gray-900 dark:text-white\"\n style={{ fontFamily: '\"Geologica\", sans-serif', animation: 'chat-fade-in 0.35s ease-out' }}\n >\n {agentName ? (\n <>\n {t('How can ')}\n <span className=\"text-[var(--chat-accent)]\">{agentName}</span>\n {t(' help you, ')}\n {firstName}?\n </>\n ) : (\n <>\n {t('How can I help you, ')}\n {firstName}?\n </>\n )}\n </h2>\n\n {agentDescription && (\n <p className=\"mb-5 max-w-[320px] text-center text-[0.75rem] leading-5 text-gray-500 dark:text-white/40\">{agentDescription}</p>\n )}\n {!agentDescription && <span className=\"mb-5\" />}\n\n <div className=\"w-full max-w-[320px]\">\n <span className=\"block text-center mb-2 text-[0.65rem] tracking-[1.5px] uppercase text-[var(--chat-accent)] font-semibold\">\n {t('Suggestions')}\n </span>\n {suggestionsLoading ? (\n // Placeholder rows rather than an empty gap: the list is about to be\n // replaced by the new agent's own suggestions.\n <div aria-hidden className=\"space-y-1\">\n {[0, 1, 2].map((i) => (\n <div key={i} className=\"h-8 rounded-lg border border-gray-200 dark:border-white/10 bg-gray-100/50 dark:bg-white/[0.03] animate-pulse\" />\n ))}\n </div>\n ) : (\n promptSuggestions.map((prompt) => (\n <button\n key={prompt}\n type=\"button\"\n onClick={() => onPromptClick(prompt)}\n className=\"w-full text-left text-[0.8125rem] text-gray-800 dark:text-white py-1.5 px-3 mb-1 rounded-lg border border-gray-200 dark:border-white/10 bg-transparent transition-colors hover:bg-[var(--chat-accent-10)] hover:border-[var(--chat-accent-50)]\"\n >\n {t(prompt)}\n </button>\n ))\n )}\n </div>\n </div>\n);\n","import { useMemo, useRef, useState } from 'react';\nimport type { ChatConversationSummary } from '../types';\nimport { timeAgo } from '../utils';\nimport { BotIcon, EditIcon, SearchIcon, SidebarIcon, TrashIcon } from './icons';\nimport { Spinner } from './Spinner';\nimport { Tooltip } from './Tooltip';\n\ninterface ConversationSidebarProps {\n conversations: ChatConversationSummary[];\n loading: boolean;\n activeConversationId: string | null;\n collapsed: boolean;\n onToggleCollapsed: () => void;\n onSelect: (id: string) => void;\n onDelete: (id: string) => void;\n /** Omit to hide the rename affordance (backend without the route). */\n onRename?: (id: string, title: string) => void;\n onNewChat: () => void;\n t: (key: string) => string;\n}\n\n/** Below this many conversations the list is quicker to scan than to filter. */\nconst SEARCH_THRESHOLD = 7;\n\n/**\n * Persistent conversation list for fullscreen mode, mirroring the XTM One web\n * chat's sidebar.\n *\n * Fullscreen is the only mode with room for it: in floating and sidebar modes\n * the header's history menu remains the way in, since a permanent column there\n * would eat most of the panel.\n */\nexport const ConversationSidebar = ({\n conversations,\n loading,\n activeConversationId,\n collapsed,\n onToggleCollapsed,\n onSelect,\n onDelete,\n onRename,\n onNewChat,\n t,\n}: ConversationSidebarProps) => {\n const [query, setQuery] = useState('');\n const [editingId, setEditingId] = useState<string | null>(null);\n const [draftTitle, setDraftTitle] = useState('');\n // Guards the input's onBlur: committing and then blurring would fire the\n // commit twice, and cancelling via Escape blurs too.\n const settledRef = useRef(false);\n\n const startRename = (id: string, current: string) => {\n settledRef.current = false;\n setEditingId(id);\n setDraftTitle(current);\n };\n\n const commitRename = () => {\n if (settledRef.current) return;\n settledRef.current = true;\n const id = editingId;\n const next = draftTitle.trim();\n setEditingId(null);\n // Unchanged or emptied: leave the conversation alone rather than issuing a\n // request that would either no-op or wipe the title.\n if (id && next) onRename?.(id, next);\n };\n\n const cancelRename = () => {\n settledRef.current = true;\n setEditingId(null);\n };\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return conversations;\n return conversations.filter((c) => (c.title || '').toLowerCase().includes(q));\n }, [conversations, query]);\n\n if (collapsed) {\n return (\n <div className=\"flex flex-col items-center gap-1 border-r border-gray-200 dark:border-white/10 px-2 py-3 shrink-0\">\n <Tooltip title={t('Show conversations')}>\n <button\n type=\"button\"\n onClick={onToggleCollapsed}\n aria-label={t('Show conversations')}\n aria-expanded={false}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg text-gray-400 dark:text-white/30 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <SidebarIcon size={17} />\n </button>\n </Tooltip>\n <Tooltip title={t('New conversation')}>\n <button\n type=\"button\"\n onClick={onNewChat}\n aria-label={t('New conversation')}\n className=\"w-8 h-8 flex items-center justify-center rounded-lg text-gray-400 dark:text-white/30 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <EditIcon size={17} />\n </button>\n </Tooltip>\n </div>\n );\n }\n\n return (\n <div className=\"w-64 shrink-0 flex flex-col border-r border-gray-200 dark:border-white/10\">\n <div className=\"flex items-center gap-1 px-3 py-3\">\n <button\n type=\"button\"\n onClick={onNewChat}\n className=\"flex-1 flex items-center gap-2 rounded-lg border border-gray-200 dark:border-white/10 px-3 py-1.5 text-[0.8125rem] text-gray-700 dark:text-white/70 hover:border-[var(--chat-accent)]/40 hover:text-[var(--chat-accent)] transition-colors\"\n >\n <EditIcon size={15} />\n {t('New conversation')}\n </button>\n <Tooltip title={t('Hide conversations')}>\n <button\n type=\"button\"\n onClick={onToggleCollapsed}\n aria-label={t('Hide conversations')}\n aria-expanded\n className=\"w-8 h-8 flex items-center justify-center shrink-0 rounded-lg text-gray-400 dark:text-white/30 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n >\n <SidebarIcon size={17} />\n </button>\n </Tooltip>\n </div>\n\n {conversations.length > SEARCH_THRESHOLD && (\n <div className=\"px-3 pb-2\">\n <div className=\"relative\">\n <SearchIcon size={12} className=\"absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 dark:text-white/40\" />\n <input\n type=\"text\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape') setQuery('');\n }}\n placeholder={t('Search conversations...')}\n aria-label={t('Search conversations...')}\n className=\"w-full h-7 pl-7 pr-2 rounded-md bg-gray-100 dark:bg-white/[0.06] text-[0.75rem] text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-white/40 outline-hidden focus:ring-1 focus:ring-[var(--chat-accent)]\"\n />\n </div>\n </div>\n )}\n\n <div className=\"flex-1 overflow-y-auto px-2 pb-3 filigran-chat-scrollable\">\n {loading && conversations.length === 0 && (\n <div className=\"px-3 py-2\">\n <Spinner size={16} />\n </div>\n )}\n {!loading && conversations.length === 0 && (\n <p className=\"px-3 py-2 text-[0.75rem] text-gray-400 dark:text-white/40\">{t('No conversations yet')}</p>\n )}\n {conversations.length > 0 && filtered.length === 0 && (\n <p className=\"px-3 py-2 text-[0.75rem] text-gray-400 dark:text-white/40\">{t('No conversation matches')}</p>\n )}\n\n {filtered.map((c) => {\n const isActive = c.conversationId === activeConversationId;\n const isEditing = c.conversationId === editingId;\n return (\n <div\n key={c.conversationId}\n role=\"button\"\n tabIndex={0}\n aria-current={isActive}\n onClick={() => onSelect(c.conversationId)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect(c.conversationId);\n }\n }}\n className={`group flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left cursor-pointer transition-colors ${\n isActive ? 'bg-[var(--chat-accent)]/10' : 'hover:bg-gray-100 dark:hover:bg-white/[0.06]'\n }`}\n >\n <span\n className={`mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-lg ${\n isActive ? 'bg-[var(--chat-accent)]/20 text-[var(--chat-accent)]' : 'bg-gray-100 dark:bg-white/[0.06] text-gray-400 dark:text-white/30'\n }`}\n >\n <BotIcon size={13} />\n </span>\n <span className=\"min-w-0 flex-1\">\n {isEditing ? (\n <input\n autoFocus\n value={draftTitle}\n onChange={(e) => setDraftTitle(e.target.value)}\n // The row selects on click and on Enter/Space; while editing\n // those belong to the field, not to navigation.\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n e.stopPropagation();\n if (e.key === 'Enter') commitRename();\n if (e.key === 'Escape') cancelRename();\n }}\n onBlur={commitRename}\n aria-label={t('Conversation title')}\n className=\"w-full rounded-md bg-white dark:bg-white/10 px-1.5 py-0.5 text-[0.8125rem] text-gray-900 dark:text-white outline-hidden ring-1 ring-[var(--chat-accent)]\"\n />\n ) : (\n <span className={`block truncate text-[0.8125rem] ${isActive ? 'text-gray-900 dark:text-white' : 'text-gray-700 dark:text-white/70'}`}>\n {c.title || t('Untitled conversation')}\n </span>\n )}\n {/*\n Agent before time, on one line: which agent a thread is with\n is what tells two similarly-titled conversations apart, and\n knowing it before opening one is the point. Omitted entirely\n when the backend does not report it, rather than padded with\n a placeholder.\n */}\n {(c.agentName || c.updatedAt) && !isEditing && (\n <span className=\"block text-[0.65rem] text-gray-400 dark:text-white/30 truncate\">\n {c.agentName}\n {c.agentName && c.updatedAt ? ' · ' : ''}\n {c.updatedAt ? timeAgo(c.updatedAt, t) : ''}\n </span>\n )}\n </span>\n {!isEditing && (\n <span className=\"flex shrink-0 self-center opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity\">\n {onRename && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n startRename(c.conversationId, c.title || '');\n }}\n aria-label={t('Rename conversation')}\n title={t('Rename conversation')}\n className=\"p-1 rounded-md text-gray-400 dark:text-white/30 hover:text-[var(--chat-accent)]\"\n >\n <EditIcon size={13} />\n </button>\n )}\n <button\n type=\"button\"\n onClick={(e) => {\n // The row is the click target for selection, so deleting\n // must not also open the conversation on its way out.\n e.stopPropagation();\n onDelete(c.conversationId);\n }}\n aria-label={t('Delete conversation')}\n title={t('Delete conversation')}\n className=\"p-1 rounded-md text-gray-400 dark:text-white/30 hover:text-red-500 dark:hover:text-red-400\"\n >\n <TrashIcon size={13} />\n </button>\n </span>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n};\n","import { type FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { ChatAttachment, ChatMessage, ChatPanelProps } from '../types';\nimport { hexAlpha, identity } from '../utils';\nimport { parseAttachments, parseContextUsage, parseToolCallTrace, parseTransferChain } from '../hooks/protocols/parseRestEvent';\nimport { useChat } from '../hooks/useChat';\nimport { useAgents } from '../hooks/useAgents';\nimport { useConversations } from '../hooks/useConversations';\nimport { useSidebarResize } from '../hooks/useSidebarResize';\nimport { useAwayCompletionNotice } from '../hooks/useAwayCompletionNotice';\nimport { useComposerExtras } from '../hooks/useComposerExtras';\nimport { useAgentSuggestions } from '../hooks/useAgentSuggestions';\nimport { DefaultLogoIcon } from './icons';\nimport { ChatHeader } from './ChatHeader';\nimport { ChatInput } from './ChatInput';\nimport { ChatMessages } from './ChatMessages';\nimport { ChatWelcome } from './ChatWelcome';\nimport { ConversationSidebar } from './ConversationSidebar';\n\nconst FLOATING_WIDTH = 380;\nconst FLOATING_HEIGHT = 560;\nconst SIDEBAR_GAP = 6;\n\nconst DEFAULT_SUGGESTIONS = [\n 'Help me create a new simulation scenario',\n 'What are the latest attack patterns?',\n 'How do I configure detection rules?',\n 'Summarize my recent findings',\n];\n\nexport const ChatPanel: FunctionComponent<ChatPanelProps> = ({\n mode,\n onClose,\n onModeChange,\n topOffset = 0,\n apiBaseUrl,\n apiEndpoints,\n agentDashboardUrl,\n user,\n t = identity,\n accentColor = '#7b5cff',\n logoIcon,\n promptSuggestions = DEFAULT_SUGGESTIONS,\n draftBorderColor,\n resizable = false,\n onWidthChange,\n onResizeStart,\n onResizeEnd,\n disableFileManagement = false,\n onRelativeLinkClick,\n onDownloadError,\n maxFileCount,\n maxTotalSize,\n requestHeaders,\n pageContext,\n pushContentSelector,\n backendType = 'rest',\n miniGameEnabled = true,\n notifyOnComplete = true,\n onTaskComplete,\n onMessageFeedback,\n disableImagePreviews = false,\n contextUsageEnabled = true,\n composerToolbar,\n}) => {\n const [modeMenuOpen, setModeMenuOpen] = useState(false);\n\n const { agents, agentsLoading, agentsError, selectedAgent, agentMenuOpen, setAgentMenuOpen, handleSwitchAgent } = useAgents({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n });\n\n const {\n messages,\n inputValue,\n setInputValue,\n isLoading,\n agentStatus,\n attachedFiles,\n conversationId,\n contextUsage,\n transferredAgent,\n canSteer,\n pendingApprovals,\n isSubmittingApproval,\n approvalError,\n submitApprovalDecisions,\n isResumingAfterDecision,\n historyReloadNonce,\n historyLoadedRef,\n conversationIdRef,\n handleFileAdd,\n handlePaste,\n handleSendMessage,\n handleNewChat,\n handleStopGenerating,\n setAttachedFiles,\n setMessages,\n setContextUsage,\n updateConversationId,\n handleSwitchConversation,\n } = useChat({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n agentSlug: selectedAgent?.slug,\n requestHeaders,\n pageContext,\n t,\n maxFileCount,\n maxTotalSize,\n });\n\n const { suggestions: agentSuggestions, loading: suggestionsLoading } = useAgentSuggestions({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n agentSlug: selectedAgent?.slug,\n });\n\n const { prompts, quota, refreshQuota } = useComposerExtras({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n });\n\n const { historyEnabled, conversations, conversationsLoading, refreshConversations, deleteConversation, renameConversation } = useConversations({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n });\n const [historyMenuOpen, setHistoryMenuOpen] = useState(false);\n\n // Fullscreen shows the conversation list permanently, so it has to be\n // fetched on entry rather than lazily when a menu opens. Kept collapsible:\n // fullscreen is also the mode people use to read a long answer.\n const [sidebarCollapsed, setSidebarCollapsed] = useState(false);\n const showConversationSidebar = mode === 'fullscreen' && historyEnabled;\n\n // Re-read on entry AND whenever the active conversation changes, so a chat\n // created by the first send appears in the list. The header menu could fetch\n // lazily on open; a permanent list has no such moment, and a stale sidebar\n // that never shows the conversation you are in is worse than no sidebar.\n useEffect(() => {\n if (showConversationSidebar) void refreshConversations();\n }, [showConversationSidebar, conversationId, refreshConversations]);\n\n // A finished turn has consumed allowance and may have retitled the chat.\n const wasLoadingRef = useRef(false);\n useEffect(() => {\n if (wasLoadingRef.current && !isLoading) {\n refreshQuota();\n // The backend titles a conversation from its first message, so the row\n // stays \"New conversation\" until the turn is read back.\n if (showConversationSidebar) void refreshConversations();\n }\n wasLoadingRef.current = isLoading;\n }, [isLoading, refreshQuota, showConversationSidebar, refreshConversations]);\n\n const handleHistoryMenuToggle = () => {\n // Computed from the committed state in the event handler — NOT inside the\n // state updater, which must stay pure (StrictMode/concurrent rendering may\n // invoke updaters more than once, which would duplicate the fetch).\n const next = !historyMenuOpen;\n if (next) {\n // Fetch lazily on open so the list reflects the latest server state\n // (titles are rewritten by the backend after the first message).\n void refreshConversations();\n }\n setHistoryMenuOpen(next);\n };\n\n const handleSelectConversation = (id: string) => {\n setHistoryMenuOpen(false);\n handleSwitchConversation(id);\n };\n\n const handleDeleteConversation = async (id: string) => {\n const deleted = await deleteConversation(id);\n // Deleting the active conversation resets to a fresh chat so the next\n // message doesn't target a dead conversation id.\n if (deleted && id === conversationIdRef.current) {\n handleNewChat();\n }\n };\n\n const { sidebarWidth, handleResizeStart, defaultWidth, isResizing } = useSidebarResize({\n mode,\n resizable,\n onWidthChange,\n onResizeStart,\n onResizeEnd,\n });\n\n // Push content when sidebar mode is active using CSS variable\n useEffect(() => {\n const width = mode === 'sidebar' ? (resizable ? sidebarWidth : defaultWidth) : 0;\n const pushWidth = width > 0 ? width + SIDEBAR_GAP : 0;\n\n // Set CSS variable on :root for any component to use\n document.documentElement.style.setProperty('--chatbot-sidebar-width', `${pushWidth}px`);\n document.documentElement.style.setProperty('--chatbot-transition', isResizing ? 'none' : 'all 225ms cubic-bezier(0.4, 0, 0.2, 1)');\n\n // Also apply to pushContentSelector if provided (for simple cases)\n if (pushContentSelector) {\n const contentElement = document.querySelector<HTMLElement>(pushContentSelector);\n if (contentElement) {\n const originalPaddingRight = contentElement.style.paddingRight;\n const originalTransition = contentElement.style.transition;\n\n contentElement.style.paddingRight = pushWidth > 0 ? `${pushWidth}px` : '';\n contentElement.style.transition = isResizing ? 'none' : 'padding-right 225ms cubic-bezier(0.4, 0, 0.2, 1)';\n\n return () => {\n contentElement.style.paddingRight = originalPaddingRight;\n contentElement.style.transition = originalTransition;\n document.documentElement.style.setProperty('--chatbot-sidebar-width', '0px');\n };\n }\n }\n\n return () => {\n document.documentElement.style.setProperty('--chatbot-sidebar-width', '0px');\n };\n }, [pushContentSelector, mode, sidebarWidth, defaultWidth, resizable, isResizing]);\n\n // Stable across renders so the memoized message rows (which take it as a\n // prop) aren't invalidated on every streamed frame.\n const resolvedLogo = useMemo(() => logoIcon ?? <DefaultLogoIcon size={24} />, [logoIcon]);\n const firstName = user.firstName;\n /**\n * The agent a *restored* conversation belongs to, as the backend reports it.\n *\n * Sits between the live transfer and the menu selection on purpose. Reopening\n * a past thread used to relabel it — and its new replies — with whichever\n * agent was selected, while the backend went on routing the conversation to\n * its own stored agent. The label named someone who had not spoken. This is\n * the same agent that will answer, so the two now agree.\n */\n const [conversationAgentName, setConversationAgentName] = useState<string | null>(null);\n const agentName = transferredAgent?.name || conversationAgentName || selectedAgent?.name || 'Assistant';\n\n // \"Viewing the chat\" must mean the panel is on screen in the active tab —\n // NOT that an element inside it currently holds focus. In sidebar (and\n // floating) mode the user routinely reads a streamed answer while their focus\n // stays in the host app, so the previous focus-within test\n // (`document.activeElement.closest('.filigran-chatbot.fixed')`) wrongly\n // classified them as \"not viewing\" and fired a redundant completion toast for\n // an answer sitting right in front of them. The host mounts `<ChatPanel/>`\n // only while the widget is open, so a mounted + visible panel root means the\n // answer is visible to the user; the notifier still treats a hidden tab or an\n // unfocused window as \"away\" and notifies there. A host that keeps the panel\n // mounted but `display:none` while \"closed\" is likewise reported as not\n // viewing (checkVisibility() === false), so completion still notifies. The\n // `.fixed` qualifier targets the panel root, never the toggle button (which\n // carries `.filigran-chatbot` alone). Stable reference (useCallback) so the\n // notifier's effect doesn't re-run on every render — the panel re-renders\n // frequently while a response streams; the DOM is queried live on each call.\n const isViewingChat = useCallback(() => {\n if (typeof document === 'undefined') return false;\n const panel = document.querySelector('.filigran-chatbot.fixed') as (HTMLElement & { checkVisibility?: () => boolean }) | null;\n if (!panel) return false;\n if (typeof panel.checkVisibility === 'function') return panel.checkVisibility();\n // Fallback for browsers without Element.checkVisibility(): a display:none\n // panel generates no layout box, so an empty client-rect list means hidden\n // (works for the position:fixed root, whose offsetParent is null even when\n // shown). Erring toward \"not viewing\" keeps the documented closed/hidden\n // path notifying instead of silently swallowing the notice on older engines.\n return panel.getClientRects().length > 0;\n }, []);\n\n // Notify when a long turn finishes and the user is not watching the chat —\n // away (tab hidden / another window / panel closed-or-hidden). An open,\n // on-screen panel in the focused tab counts as watching, so no toast fires\n // for an answer the user can already see.\n useAwayCompletionNotice({\n isLoading,\n agentName,\n t,\n enabled: notifyOnComplete,\n onComplete: onTaskComplete,\n isViewingChat,\n });\n\n // Download an agent-generated file. The URL is resolved against the host\n // app's own backend proxy (apiBaseUrl), NOT the upstream chat service:\n // the proxy mints any upstream token server-side, so the user stays\n // authenticated to the host platform (e.g. OpenCTI / OpenAEV) only and\n // never logs in to the upstream service. Same-origin cookies +\n // requestHeaders (CSRF / draft context) carry the host-app auth.\n // Downloads need a path to build the URL from. Enabled for the REST\n // backend unless explicitly disabled (`download === null`). In\n // single-endpoint mode there is no per-path routing, so a download path\n // must be provided explicitly (e.g. an OpenCTI-style proxy route);\n // otherwise the default REST `/chat/files` path is used.\n const downloadPathProvided = apiEndpoints?.download !== null && apiEndpoints?.download !== undefined;\n const canDownload = backendType === 'rest' && apiEndpoints?.download !== null && (!apiEndpoints?.singleEndpoint || downloadPathProvided);\n\n // The host-proxied URL of an attachment. Shared by the download action and\n // the inline image preview so both resolve against the same proxy route.\n const resolveAttachmentUrl = useCallback(\n (att: ChatAttachment) => {\n const base = apiEndpoints?.download ?? '/chat/files';\n return `${apiBaseUrl}${base}/${encodeURIComponent(att.fileId)}/download`;\n },\n [apiBaseUrl, apiEndpoints],\n );\n\n const handleDownloadFile = useCallback(\n async (att: ChatAttachment) => {\n const url = resolveAttachmentUrl(att);\n try {\n const res = await fetch(url, {\n method: 'GET',\n credentials: 'include',\n headers: { ...(requestHeaders ?? {}) },\n });\n if (!res.ok) throw new Error(`Download failed: ${res.status}`);\n const blob = await res.blob();\n const objectUrl = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = objectUrl;\n link.download = att.filename || 'download';\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(objectUrl);\n } catch (err) {\n // Surface the failure (403/404/5xx/network) to the host so it can\n // notify the user — the chatbot has no toast surface of its own.\n // If the host doesn't provide a handler the error is intentionally\n // not thrown further (a rejected click handler has nowhere to go).\n onDownloadError?.(err, att);\n }\n },\n [resolveAttachmentUrl, requestHeaders, onDownloadError],\n );\n\n const cssVars = {\n '--chat-accent': accentColor,\n '--chat-accent-10': hexAlpha(accentColor, 0.1),\n '--chat-accent-40': hexAlpha(accentColor, 0.25),\n '--chat-accent-50': hexAlpha(accentColor, 0.5),\n '--chat-accent-dark': accentColor,\n } as React.CSSProperties;\n\n // Tracks whether this panel is still mounted. Flipped to false ONLY on a\n // real unmount (empty-deps cleanup) — never on the benign teardowns that an\n // inline `apiEndpoints` / `requestHeaders` prop churn or a StrictMode\n // double-invoke trigger. The restore effect below reads it so an in-flight\n // `/chat/sessions` response that outlives the panel — the host renders\n // `<ChatPanel />` conditionally and the user closes it mid-request — can't\n // call `setMessages` / `updateConversationId` after unmount, while a restore\n // merely interrupted by a re-render still lands. Re-set to true on setup so\n // StrictMode's mount → unmount → remount of the same instance leaves it true.\n const isMountedRef = useRef(true);\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n // Load conversation history when agent is selected\n useEffect(() => {\n // Skip session history if disabled, using single endpoint mode, or non-REST backend\n if (apiEndpoints?.sessions === null || apiEndpoints?.singleEndpoint || backendType === 'legacy' || backendType === 'ag-ui') return;\n // No conversation means a fresh chat — \"New conversation\", or an agent\n // switch, both of which null the id. Drop the restored thread's agent with\n // it, or the new chat would keep wearing the old one's name.\n if (!conversationId) {\n setConversationAgentName(null);\n return;\n }\n if (historyLoadedRef.current || !selectedAgent) return;\n historyLoadedRef.current = true;\n const sessionsUrl = `${apiBaseUrl}${apiEndpoints?.sessions ?? '/chat/sessions'}`;\n\n // The conversation this restore is being issued for. A host re-render that\n // churns an inline `apiEndpoints` / `requestHeaders` prop, or a React\n // StrictMode double-invoke, tears this effect down and re-runs it while the\n // request is still in flight — but the conversation itself hasn't changed.\n // We must NOT drop the restore in those benign cases (doing so left the\n // panel looking like a brand-new chat, randomly, on reload). Only a real\n // change — the user starting a new chat or switching agent, both of which\n // reset the id via `handleNewChat()` — should abandon the response, so it\n // can't resurrect a dead id or overwrite the freshly-started conversation.\n // Compare the live ref at apply time (not a blanket teardown flag) so the\n // legitimate restore always lands while a superseded one is still ignored —\n // and bail out entirely once the panel has actually unmounted, so a late\n // response can't write to localStorage / state after the panel is gone.\n const requestedConversationId = conversationId;\n const isStale = () => !isMountedRef.current || conversationIdRef.current !== requestedConversationId;\n\n fetch(sessionsUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({\n conversation_id: conversationId,\n agent_slug: selectedAgent.slug,\n }),\n })\n .then((res) => {\n if (isStale()) return null;\n if (!res.ok) {\n // Stale or invalid stored id (e.g. the platform was reset but the\n // browser kept an old id) — silently reset so a fresh conversation\n // is created on the next message instead of surfacing an error and\n // forcing the user to click \"New conversation\".\n updateConversationId(null);\n return null;\n }\n return res.json();\n })\n .then((data) => {\n if (!data || isStale()) return;\n // The backend resolves the session: it returns the same id when the\n // conversation still exists, or transparently creates a fresh one and\n // returns its NEW id when the stored id is stale. Adopt whatever id it\n // returns (and persist it) so we never send subsequent messages\n // against a dead conversation — which would 404 with\n // \"conversation does not exist\".\n if (typeof data.conversation_id === 'string' && data.conversation_id && data.conversation_id !== requestedConversationId) {\n updateConversationId(data.conversation_id);\n }\n // Null is meaningful and different from absent: it says the backend\n // knows this conversation has no agent (one predating per-conversation\n // routing), where a missing key means an older backend that cannot\n // tell us. Both land on the selected-agent fallback, but only the\n // first is a deliberate answer.\n setConversationAgentName(typeof data.agent_name === 'string' ? data.agent_name : null);\n if (!data.messages?.length) return;\n const restored: ChatMessage[] = data.messages.map(\n (\n m: {\n role: string;\n content: string;\n attachments?: unknown;\n tool_names?: unknown;\n tool_call_count?: unknown;\n iterations?: unknown;\n reasoning?: unknown;\n tool_call_trace?: unknown;\n transfer_chain?: unknown;\n is_truncated?: unknown;\n agent_name?: unknown;\n context_tokens?: unknown;\n context_window?: unknown;\n context_breakdown?: unknown;\n },\n i: number,\n ) => ({\n id: `restored-${i}`,\n role: m.role as 'user' | 'assistant',\n content: m.content,\n timestamp: new Date(),\n // Per-message attribution when the backend keeps it — the only way\n // a thread that changed hands mid-way reads correctly. Nothing\n // records it today, so this is normally undefined and the\n // conversation's agent applies to the whole thread.\n agentName: typeof m.agent_name === 'string' ? m.agent_name : undefined,\n // Re-surface downloadable file chips on conversation restore for\n // both roles: agent-generated deliverables on assistant messages\n // (the [[FILE:…]] markers in content are stripped at render time by\n // ChatMessages) and user-uploaded files on user messages (so an\n // upload stays downloadable after a page reload, not just in the\n // live session where it is carried on `files`).\n attachments: parseAttachments(m.attachments),\n // Re-surface the reasoning-details affordance (\"i\" button) on\n // restored assistant messages — same fields the live `done`\n // event carries.\n toolNames: Array.isArray(m.tool_names) ? (m.tool_names as string[]) : undefined,\n toolCallCount: typeof m.tool_call_count === 'number' ? m.tool_call_count : undefined,\n iterations: typeof m.iterations === 'number' ? m.iterations : undefined,\n reasoning: typeof m.reasoning === 'string' ? m.reasoning : undefined,\n toolCallTrace: parseToolCallTrace(m.tool_call_trace),\n transferChain: parseTransferChain(m.transfer_chain),\n isTruncated: m.is_truncated === true || undefined,\n }),\n );\n setMessages(restored);\n // The context gauge is conversation state, not per-message: the NEWEST\n // entry that carries a reading is the current occupancy. Scanned from\n // the end so a turn that predates the field (or a user message) falls\n // through to the last one that has it, rather than blanking the gauge.\n for (let i = data.messages.length - 1; i >= 0; i -= 1) {\n const usage = parseContextUsage(data.messages[i] as Record<string, unknown>);\n if (usage) {\n setContextUsage(usage);\n break;\n }\n }\n })\n .catch(() => {\n if (isStale()) return;\n updateConversationId(null);\n });\n }, [\n conversationId,\n selectedAgent,\n apiBaseUrl,\n apiEndpoints,\n backendType,\n // Re-runs the restore when the hook asks for one — the only way a turn\n // resumed without a stream can ever show its answer.\n historyReloadNonce,\n historyLoadedRef,\n conversationIdRef,\n isMountedRef,\n requestHeaders,\n setMessages,\n setContextUsage,\n updateConversationId,\n ]);\n\n const onSwitchAgent = (agent: typeof selectedAgent) => {\n if (!agent) return;\n handleSwitchAgent(agent, () => {\n handleNewChat();\n });\n };\n\n const containerClasses = (() => {\n const base = 'filigran-chatbot';\n switch (mode) {\n case 'sidebar':\n return `${base} fixed right-0 bottom-0 flex flex-col bg-white dark:bg-[#1e1e2e] border-l border-gray-200 dark:border-white/10 z-[1200]`;\n case 'floating':\n return `${base} fixed bottom-5 right-5 flex flex-col bg-white dark:bg-[#1e1e2e] rounded-xl shadow-[0_8px_32px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_32px_rgba(0,0,0,0.4)] z-[1300] border border-gray-200 dark:border-white/10`;\n case 'fullscreen':\n return `${base} fixed right-0 bottom-0 left-0 flex flex-col bg-gray-50 dark:bg-[#161622] z-[1400]`;\n default:\n return base;\n }\n })();\n\n const containerStyle: React.CSSProperties = {\n ...cssVars,\n ...(mode === 'sidebar'\n ? { top: topOffset, width: resizable ? sidebarWidth : defaultWidth }\n : mode === 'floating'\n ? { width: FLOATING_WIDTH, height: FLOATING_HEIGHT }\n : { top: topOffset }),\n };\n\n return (\n <div className={containerClasses} style={containerStyle}>\n {mode === 'sidebar' && resizable && (\n <div onMouseDown={handleResizeStart} className=\"absolute top-0 -left-1 bottom-0 w-2 cursor-col-resize z-10 group\">\n <div className=\"absolute top-0 left-1/2 -translate-x-1/2 bottom-0 w-0.5 rounded-sm bg-[var(--chat-accent)] opacity-0 transition-opacity group-hover:opacity-100 group-active:opacity-100\" />\n </div>\n )}\n <ChatHeader\n mode={mode}\n agentName={agentName}\n agents={agents}\n agentsLoading={agentsLoading}\n agentsError={agentsError}\n selectedAgent={selectedAgent}\n transferredFrom={transferredAgent ? selectedAgent?.name : undefined}\n agentMenuOpen={agentMenuOpen}\n onAgentMenuToggle={() => setAgentMenuOpen((p) => !p)}\n onAgentMenuClose={() => setAgentMenuOpen(false)}\n onSwitchAgent={onSwitchAgent}\n modeMenuOpen={modeMenuOpen}\n onModeMenuToggle={() => setModeMenuOpen((p) => !p)}\n onModeMenuClose={() => setModeMenuOpen(false)}\n onModeChange={onModeChange}\n onNewChat={handleNewChat}\n onClose={onClose}\n logoIcon={resolvedLogo}\n agentDashboardUrl={agentDashboardUrl}\n historyEnabled={historyEnabled && !showConversationSidebar}\n historyMenuOpen={historyMenuOpen}\n onHistoryMenuToggle={handleHistoryMenuToggle}\n onHistoryMenuClose={() => setHistoryMenuOpen(false)}\n conversations={conversations}\n conversationsLoading={conversationsLoading}\n activeConversationId={conversationId}\n onSelectConversation={handleSelectConversation}\n onDeleteConversation={(id) => void handleDeleteConversation(id)}\n t={t}\n />\n {/* Fullscreen puts the conversation list beside the thread. The header\n stays full width above both: it carries the agent picker, the mode\n switcher and close, which belong to the panel rather than to either\n column. `min-h-0` lets the thread scroll instead of growing the row. */}\n <div className={showConversationSidebar ? 'flex flex-1 min-h-0' : 'contents'}>\n {showConversationSidebar && (\n <ConversationSidebar\n conversations={conversations}\n loading={conversationsLoading}\n activeConversationId={conversationId}\n collapsed={sidebarCollapsed}\n onToggleCollapsed={() => setSidebarCollapsed((v) => !v)}\n onSelect={handleSelectConversation}\n onDelete={(id) => void handleDeleteConversation(id)}\n onRename={(id, title) => void renameConversation(id, title)}\n onNewChat={handleNewChat}\n t={t}\n />\n )}\n <div className={showConversationSidebar ? 'flex flex-1 flex-col min-w-0' : 'contents'}>\n {messages.length === 0 ? (\n <ChatWelcome\n firstName={firstName}\n logoIcon={resolvedLogo}\n // The agent's own suggestions when the backend serves them, the\n // host's list otherwise — never an empty section.\n promptSuggestions={agentSuggestions ?? promptSuggestions}\n suggestionsLoading={suggestionsLoading}\n agentName={selectedAgent?.name}\n agentDescription={selectedAgent?.description}\n onPromptClick={setInputValue}\n t={t}\n />\n ) : (\n <ChatMessages\n messages={messages}\n isLoading={isLoading}\n agentStatus={agentStatus}\n agentName={agentName}\n logoIcon={resolvedLogo}\n onRelativeLinkClick={onRelativeLinkClick}\n onDownloadFile={canDownload ? handleDownloadFile : undefined}\n resolveAttachmentUrl={canDownload && !disableImagePreviews ? resolveAttachmentUrl : undefined}\n requestHeaders={requestHeaders}\n miniGameEnabled={miniGameEnabled}\n onMessageFeedback={onMessageFeedback}\n isResumingAfterDecision={isResumingAfterDecision}\n pendingApprovals={pendingApprovals}\n onSubmitApprovalDecisions={submitApprovalDecisions}\n isSubmittingApproval={isSubmittingApproval}\n approvalError={approvalError}\n t={t}\n />\n )}\n <ChatInput\n inputValue={inputValue}\n onInputChange={setInputValue}\n onSend={handleSendMessage}\n onStop={handleStopGenerating}\n isLoading={isLoading}\n canSteer={canSteer}\n attachedFiles={disableFileManagement ? [] : attachedFiles}\n onFileAdd={disableFileManagement ? undefined : handleFileAdd}\n onFileRemove={disableFileManagement ? undefined : (i) => setAttachedFiles((prev) => prev.filter((_, j) => j !== i))}\n onPaste={disableFileManagement ? undefined : handlePaste}\n t={t}\n mode={mode}\n separatorColor={draftBorderColor}\n prompts={prompts}\n quota={quota}\n contextUsage={contextUsageEnabled ? contextUsage : null}\n composerToolbar={composerToolbar}\n />\n </div>\n </div>\n </div>\n );\n};\n","import type { FunctionComponent } from 'react';\nimport type { ChatToggleButtonProps } from '../types';\nimport { hexAlpha } from '../utils';\nimport { DefaultLogoIcon } from './icons';\n\nexport const ChatToggleButton: FunctionComponent<ChatToggleButtonProps> = ({\n isOpen,\n onToggle,\n label = 'Ask Assistant',\n accentColor = '#7b5cff',\n icon,\n}) => {\n const resolvedIcon = icon ?? <DefaultLogoIcon size={16} />;\n\n return (\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"filigran-chatbot inline-flex items-center gap-1.5 px-3 py-[3px] text-[0.8125rem] font-medium whitespace-nowrap rounded-md border transition-colors\"\n style={{\n borderColor: isOpen ? accentColor : hexAlpha(accentColor, 0.5),\n color: accentColor,\n backgroundColor: isOpen ? hexAlpha(accentColor, 0.1) : 'transparent',\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.borderColor = accentColor;\n e.currentTarget.style.backgroundColor = hexAlpha(accentColor, 0.1);\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.borderColor = isOpen ? accentColor : hexAlpha(accentColor, 0.5);\n e.currentTarget.style.backgroundColor = isOpen ? hexAlpha(accentColor, 0.1) : 'transparent';\n }}\n >\n <span className=\"[&>svg]:w-4 [&>svg]:h-4\">{resolvedIcon}</span>\n {label}\n </button>\n );\n};\n"],"names":["hexAlpha","hex","alpha","Math","round","toString","padStart","SAFE_URL_PROTOCOLS","Set","markdownUrlTransform","url","colon","indexOf","slash","question","hash","protocol","slice","toLowerCase","has","test","identity","key","findChatbotRoot","el","node","classList","contains","parentElement","document","body","compactCount","n","toFixed","replace","timeAgo","iso","t","then","Date","getTime","Number","isNaN","diffMs","now","minutes","floor","hours","days","toLocaleDateString","undefined","month","day","FILE_MARKER_RE","PARTIAL_FILE_MARKER_RE","stripFileMarkers","content","stripped","trim","BREAKDOWN_KEYS","parseContextUsage","evt","used","context_tokens","limit","context_window","isFinite","breakdown","raw","src","out","any","wireKey","field","value","parseBreakdown","context_breakdown","parseAttachments","Array","isArray","item","a","fileId","file_id","push","filename","type","size","contentType","content_type","fileTag","file_tag","length","parseToolCallTrace","e","name","input","output","success","parseTransferChain","agent_name","agentId","agent_id","agentName","parseToolApprovalProposals","p","toolCallId","tool_call_id","args","arguments","schema","input_schema","toolName","tool_name","toolDescription","tool_description","inputSchema","source","parseRestEvent","ctx","action","st","status","thinkingContent","hasUsedTools","tools","elapsedS","elapsed_s","contextUsage","proposals","conversationId","conversation_id","toolNames","tool_names","toolCallCount","tool_call_count","iterations","transferAgentId","transfer_agent_id","transferAgentName","transfer_agent_name","attachments","reasoning","toolCallTrace","tool_call_trace","transferChain","transfer_chain","isTruncated","is_truncated","parseLegacyEvent","eventType","event","data","nodeId","activeNodeId","usedTools","map","tool","chatId","parseAgUiEvent","message","stepName","delta","toolCallName","STORAGE_KEY","LEGACY_CHAT_ID_KEY","draftKey","loadDraft","window","sessionStorage","getItem","persistDraft","setItem","removeItem","DEFAULT_MAX_TOTAL_SIZE","useChat","apiBaseUrl","apiEndpoints","backendType","agentSlug","requestHeaders","pageContext","maxFileCount","maxTotalSize","isLegacy","messages","setMessages","useState","isLoading","setIsLoading","agentStatus","setAgentStatus","setConversationId","localStorage","inputValue","setInputValue","attachedFiles","setAttachedFiles","transferredAgent","setTransferredAgent","setContextUsage","pendingApprovals","setPendingApprovals","isSubmittingApproval","setIsSubmittingApproval","approvalError","setApprovalError","isResumingAfterDecision","setIsResumingAfterDecision","historyReloadNonce","setHistoryReloadNonce","resumeTick","setResumeTick","legacyChatId","setLegacyChatId","historyLoadedRef","useRef","abortControllerRef","hasUsedToolsRef","conversationIdRef","isLoadingRef","current","pageContextRef","approvalConversationIdRef","approvalDetachedRef","probedConversationRef","resumeDeadlineRef","creatingSessionRef","uploadAbortRef","AbortController","effectiveMaxFileCount","effectiveMaxTotalSize","getSteerUrl","singleEndpoint","steer","getApproveUrl","path","approve","getPendingApprovalsUrl","convId","base","readPendingApprovals","async","res","fetch","headers","ok","json","turnRunning","turn","reloadHistory","useCallback","getUploadUrl","upload","updateConversationId","id","ensureConversation","slug","sessionsUrl","sessions","promise","method","JSON","stringify","agent_slug","uploadSingleFile","file","signal","uploadUrl","formData","FormData","append","uploadHeaders","Object","fromEntries","entries","filter","k","Error","ids","file_ids","handleFileAdd","fileList","candidates","from","tempId","crypto","randomUUID","accepted","prev","currentCount","currentSize","reduce","sum","f","slotsAvailable","sizeLeft","filtered","c","newEntries","rawFile","uploadStatus","setTimeout","err","DOMException","handleNewChat","abort","useEffect","clearTimeout","draftFlushRef","state","setInterval","catch","clearInterval","cancelled","canSteer","submitApprovalDecisions","decisions","approveUrl","d","decision","verdict","rejectionReason","rejection_reason","handlePaste","files","clipboardData","preventDefault","handleSendMessage","steerText","steerUrl","optimistic","role","timestamp","m","steerMessage","userMsg","assistantId","currentAssistantId","controller","fileIds","requestBody","opts","streaming","threadId","runId","context","forwardedProps","supportsToolApproval","supports_tool_approval","keys","serialized","buildRequestBody","parseEvent","getParser","reader","getReader","decoder","TextDecoder","buffer","accumulated","doneReceived","ensureSegment","segmentId","done","read","decode","stream","lines","split","pop","rawLine","line","startsWith","jsonStr","parsed","parse","segId","elapsedStartMs","text","finalContent","handleStopGenerating","handleSwitchConversation","STORAGE_AGENT_KEY","useAgents","agents","setAgents","agentsLoading","setAgentsLoading","agentsError","setAgentsError","selectedAgent","setSelectedAgent","agentMenuOpen","setAgentMenuOpen","agentsUrl","list","parseAgents","savedSlug","match","find","finally","handleSwitchAgent","agent","onSwitch","parseConversation","title","updatedAt","updated_at","created_at","messageCount","message_count","SIDEBAR_WIDTH","SIDEBAR_WIDTH_STORAGE_KEY","flashTimer","originalTitle","activeHooks","stopTitleFlash","useAwayCompletionNotice","enabled","onComplete","isViewingChat","wasLoadingRef","startRef","clearOnReturn","hidden","hasFocus","addEventListener","removeEventListener","max","wasLoading","unfocused","away","viewingChat","showMessage","startTitleFlash","Notification","permission","tag","notifyOS","useComposerExtras","prompts","setPrompts","quota","setQuota","restLike","promptsPath","quotaPath","promptsUrl","quotaUrl","credentials","rawList","description","parsePrompts","quotaNonce","setQuotaNonce","refreshQuota","q","period","parseQuota","useAgentSuggestions","suggestions","setSuggestions","loading","setLoading","baseUrl","encodeURIComponent","s","o","v","prompt","label","parseSuggestions","AlertTriangleIcon","className","_jsxs","xmlns","width","height","viewBox","fill","stroke","strokeWidth","strokeLinecap","strokeLinejoin","children","_jsx","ArrowRightLeftIcon","AttachFileIcon","BotIcon","x","y","rx","BrainIcon","CheckCircleIcon","cx","cy","r","CheckIcon","ChevronDownIcon","CloseIcon","CopyIcon","ry","DatabaseIcon","DefaultLogoIcon","DownloadIcon","points","x1","x2","y1","y2","EditIcon","ExternalLinkIcon","FileIcon","FloatingIcon","FullscreenExitIcon","FullscreenIcon","GamepadIcon","GlobeIcon","HistoryIcon","ImageIcon","InfoIcon","MailIcon","MaximizeIcon","MicIcon","MicOffIcon","SearchIcon","SendIcon","SidebarIcon","SparklesIcon","StopCircleIcon","TerminalIcon","ThumbsDownIcon","filled","ThumbsUpIcon","TrashIcon","UserPlusIcon","WrenchIcon","XCircleIcon","Dropdown","open","onClose","anchorRef","placement","panelRef","pos","setPos","top","left","stableOnClose","ref","handler","active","listener","target","useClickOutside","useLayoutEffect","rect","getBoundingClientRect","panelHeight","offsetHeight","spaceBelow","innerHeight","bottom","spaceAbove","preferredLeft","right","min","innerWidth","portalTarget","createPortal","style","Spinner","Tooltip","show","setShow","below","setBelow","onMouseEnter","rootTop","flip","onMouseLeave","modeOptions","mode","getIcon","ChatHeader","transferredFrom","onAgentMenuToggle","onAgentMenuClose","onSwitchAgent","modeMenuOpen","onModeMenuToggle","onModeMenuClose","onModeChange","onNewChat","logoIcon","agentDashboardUrl","historyEnabled","historyMenuOpen","onHistoryMenuToggle","onHistoryMenuClose","conversations","conversationsLoading","activeConversationId","onSelectConversation","onDeleteConversation","agentAnchorRef","modeAnchorRef","historyAnchorRef","agentQuery","setAgentQuery","filteredAgents","useMemo","includes","showAgentSearch","CurrentModeIcon","onClick","autoFocus","onChange","onKeyDown","placeholder","_Fragment","conv","isActive","when","opt","getSpeechRecognition","w","SpeechRecognition","webkitSpeechRecognition","SIZE","CIRCUMFERENCE","PI","ROWS","color","ContextUsageIndicator","usage","setOpen","ratio","pruning","compacting","ringColor","textColor","percent","counts","headline","summary","rows","expandable","gauge","strokeDasharray","strokeDashoffset","transform","String","row","backgroundColor","borderRadius","PromptPicker","onPick","query","setQuery","close","QuotaIndicator","compact","exhausted","nearLimit","barColor","Boolean","join","ChatInput","onInputChange","onSend","onStop","onFileAdd","onFileRemove","onPaste","separatorColor","composerToolbar","fileInputRef","textareaRef","dictation","onFinalText","listening","setListening","interim","setInterim","recognitionRef","shouldRestartRef","onFinalTextRef","supported","Ctor","recognition","continuous","interimResults","lang","navigator","language","onresult","interimText","finalText","i","resultIndex","results","result","isFinal","transcript","onerror","onend","start","stop","toggle","useDictation","trimEnd","hasToolbar","isFileManagementEnabled","hasContent","hasFilesUploading","some","canSend","hasAttachments","showSteerSend","footerText","borderTopColor","borderTopWidth","multiple","click","scrollHeight","shiftKey","maxHeight","disabled","focus","formatValue","useRevealIntoView","raf","requestAnimationFrame","scrollIntoView","behavior","block","inline","cancelAnimationFrame","BUTTON_BASE","ApprovalCard","proposal","onDecide","rejecting","setRejecting","reason","setReason","reasonId","useId","rejectPanelRef","argumentNames","decide","next","properties","entry","argumentSchema","htmlFor","ChatApprovalPrompt","onSubmit","isSubmitting","error","setDecisions","submitted","sent","setSent","busy","submit","all","decidedCount","allDecided","willRunAlways","values","footerRef","rootRef","every","ImageLightbox","alt","hostRef","closeButtonRef","root","setRoot","previouslyFocused","activeElement","HTMLElement","preventScroll","stopPropagation","ChatImage","maxHeightClass","needsAuth","isRelativeUrl","blobUrl","setBlobUrl","errored","setErrored","expanded","setExpanded","objectUrl","blob","URL","createObjectURL","revokeObjectURL","displaySrc","onError","DEFAULT_MESSAGES","PREF_KEY","arcadeFont","px","INVADER_FRAMES","readPref","createInvaderGame","canvas","onMessage","maybeCtx","getContext","cssW","fontPx","accent","msgIndex","letters","targetIndex","bullets","particles","shipX","cooldown","clearedAt","legFrame","legTimer","last","performance","letterY","cssH","firstAlive","alive","layout","font","measureText","widths","ch","total","b","lx","char","letterCenter","applySize","dpr","devicePixelRatio","setTransform","getComputedStyle","getPropertyValue","resolveAccent","ro","ResizeObserver","observe","loop","dt","dtf","targetX","abs","ang","random","spd","vx","cos","vy","sin","life","splice","update","clearRect","fillStyle","textBaseline","globalAlpha","l","fillText","fillRect","frame","ox","SPRITE_W","draw","disconnect","ChatWaitingGame","reducedMotion","reduced","setReduced","matchMedia","matches","mql","addListener","removeListener","usePrefersReducedMotion","minigameOn","setMinigameOn","setMsgIndex","canvasRef","playMode","scroller","oy","overflowY","clientHeight","findScrollParent","scrollTop","animation","on","writePref","cleanReasoningText","ThinkingTextBubble","isOverflowing","setIsOverflowing","cleaned","formatElapsed","seconds","ChatThinking","miniGameEnabled","StatusIcon","showDots","rawNames","lower","count","display","toUpperCase","unique","consultName","checkCount","fetchCount","targetName","resolveStatusVisual","nowMs","setNowMs","showElapsed","stalled","delayMs","setStalled","prevSignalRef","signalChanged","useStalled","showGame","delay","isRelativeHref","href","MarkdownMessage","memo","onRelativeLinkClick","copiedBlock","setCopiedBlock","processedContent","hardenNestedCodeFences","fenceRe","markupLang","openerIdx","maxRun","nestedCount","lastBareFence","fence","repeat","om","cm","splitCells","endsWith","cells","isDelimiterRow","alignOf","cell","fenceChar","fenceLen","listMarkerRe","fenceMatch","run","header","delim","markerMatch","offset","trimStart","indent","headerCols","delimCells","aligns","normalizeMarkdownTables","trimmed","wrapBareJson","_match","Markdown","remarkPlugins","remarkGfm","remarkBreaks","urlTransform","components","pre","code","exec","codeStr","handleCopyCode","clipboard","writeText","ul","ol","li","strong","em","hr","blockquote","internalHref","location","origin","pathname","search","toInternalHref","routeInternally","openInNewTab","rel","img","h1","h2","h3","table","tr","th","td","displayName","prettyTraceValue","toolDisplayName","ToolCallRow","index","inputDisplay","outputDisplay","hasInput","ReasoningDetailsDialog","msg","dialogRef","dialog","focusable","querySelectorAll","first","totalCalls","transfers","trace","summaryParts","tn","fileExtensionLabel","dot","lastIndexOf","ext","IMAGE_EXTENSIONS","MessageCopyButton","copied","setCopied","MessageFeedbackButtons","buttonClass","MessageRow","isStreaming","onDownloadFile","resolveAttachmentUrl","feedback","onFeedbackChange","showReasoning","setShowReasoning","isAssistant","isEmpty","renderAttachmentCard","att","previewUrl","isImageAttachment","isWorking","sizeLabel","bytes","renderFileChip","hasReasoningDetails","showActions","blocks","seen","forEach","add","buildUserFileBlocks","parts","re","lastIndex","tail","splitFileMarkers","attByFileId","Map","part","get","buildAssistantBlocks","ChatMessages","onMessageFeedback","onSubmitApprovalDecisions","messagesEndRef","renderWindow","setRenderWindow","feedbackByMessage","setFeedbackByMessage","thinkingLen","firstMessageId","hasEarlierMessages","visibleMessages","handleFeedbackChange","messageId","rest","streamingMessageId","awaitingApproval","isStreamingMessage","ChatWelcome","firstName","promptSuggestions","onPromptClick","agentDescription","suggestionsLoading","fontFamily","ConversationSidebar","collapsed","onToggleCollapsed","onSelect","onDelete","onRename","editingId","setEditingId","draftTitle","setDraftTitle","settledRef","commitRename","isEditing","tabIndex","onBlur","DEFAULT_SUGGESTIONS","ChatPanel","topOffset","user","accentColor","draftBorderColor","resizable","onWidthChange","onResizeStart","onResizeEnd","disableFileManagement","onDownloadError","pushContentSelector","notifyOnComplete","onTaskComplete","disableImagePreviews","contextUsageEnabled","setModeMenuOpen","agentSuggestions","refreshConversations","deleteConversation","renameConversation","setConversations","setConversationsLoading","history","previous","useConversations","setHistoryMenuOpen","sidebarCollapsed","setSidebarCollapsed","showConversationSidebar","handleSelectConversation","handleDeleteConversation","sidebarWidth","handleResizeStart","defaultWidth","isResizing","setSidebarWidth","stored","parseInt","setIsResizing","isResizingRef","sidebarWidthRef","onWidthChangeRef","onResizeEndRef","handleMouseMove","newWidth","clientX","maxWidth","clamped","handleMouseUp","cursor","userSelect","handleWindowResize","useSidebarResize","pushWidth","documentElement","setProperty","contentElement","querySelector","originalPaddingRight","paddingRight","originalTransition","transition","resolvedLogo","conversationAgentName","setConversationAgentName","panel","checkVisibility","getClientRects","downloadPathProvided","download","canDownload","handleDownloadFile","link","createElement","appendChild","remove","cssVars","isMountedRef","requestedConversationId","isStale","restored","containerClasses","containerStyle","onMouseDown","_","j","ChatToggleButton","isOpen","onToggle","icon","resolvedIcon","borderColor","currentTarget"],"mappings":"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"}