@filigran/chatbot 3.13.0 → 3.15.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/utils/feedback.ts","../../src/hooks/protocols/parseRestEvent.ts","../../src/hooks/protocols/parseLegacyEvent.ts","../../src/hooks/protocols/parseAgUiEvent.ts","../../src/utils/pageActions.ts","../../src/utils/pageSnapshot.ts","../../src/hooks/useChat.ts","../../src/hooks/streamDeltas.ts","../../src/hooks/useAgents.ts","../../src/hooks/useConversations.ts","../../src/hooks/useSidebarResize.ts","../../src/hooks/useAwayCompletionNotice.ts","../../src/hooks/useComposerExtras.ts","../../src/utils/quota.ts","../../src/hooks/useAgentSuggestions.ts","../../src/hooks/useStableProps.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/VolumeIcon.tsx","../../src/components/icons/WrenchIcon.tsx","../../src/components/icons/XCircleIcon.tsx","../../src/components/Dropdown.tsx","../../src/hooks/useClickOutside.ts","../../src/components/Spinner.tsx","../../src/utils/tooltipPlacement.ts","../../src/components/Tooltip.tsx","../../src/components/ChatHeader.tsx","../../src/hooks/dictationController.ts","../../src/hooks/useDictation.ts","../../src/components/ContextUsageIndicator.tsx","../../src/components/PromptPicker.tsx","../../src/components/QuotaIndicator.tsx","../../src/components/ChatInput.tsx","../../src/hooks/useMessageFeedback.ts","../../src/utils/speech.ts","../../src/hooks/useSpeechReader.ts","../../src/utils/messageTime.ts","../../src/utils/scrollFollow.ts","../../src/components/ChatApprovalPrompt.tsx","../../src/components/ChatImage.tsx","../../src/components/ChatWaitingPanel.tsx","../../src/components/ChatThinking.tsx","../../src/utils/markdownBlocks.ts","../../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":["import type { Translate } from '../types';\n\nexport 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 // Find the outer block's *matching* closer rather than the last bare fence in\n // the message. Same-length fences are ambiguous — an inner ``` looks exactly\n // like the outer closer — so inner fences are paired off with two structural\n // signals:\n // • a labelled fence (```lang) opens a nested block, and the next bare\n // fence closes it (depth tracking); and\n // • two consecutive *bare* fences wrapping a non-blank body form an\n // unlabelled nested code block — a ``` … ``` example inside the document.\n // A bare fence at depth 0 is the outer closer when the next fence is\n // labelled, absent, or bare-but-separated-by-only-blank-lines: that last case\n // is a block boundary (this closer followed by a *separate* block), not\n // nesting, so the separate block is never absorbed.\n //\n // Taking the last bare fence instead swallowed everything up to it — trailing\n // prose and unrelated code blocks alike — whenever a later block followed the\n // markdown one.\n const fences: { idx: number; run: number; bare: boolean }[] = [];\n for (let i = openerIdx + 1; i < lines.length; i++) {\n const m = lines[i].match(fenceRe);\n if (m) fences.push({ idx: i, run: m[2].length, bare: m[3].trim() === '' });\n }\n\n // True when every line strictly between two fence lines is blank — which\n // separates a nested code block (real body) from a mere block boundary.\n const blankBetween = (a: number, b: number): boolean => {\n for (let k = a + 1; k < b; k++) {\n if (lines[k].trim() !== '') return false;\n }\n return true;\n };\n\n let depth = 0;\n let maxRun = 3;\n let nestedCount = 0;\n let closerIdx = -1;\n for (let j = 0; j < fences.length; j++) {\n const f = fences[j];\n if (f.bare && depth === 0) {\n const next = fences[j + 1];\n if (!next || !next.bare || blankBetween(f.idx, next.idx)) {\n closerIdx = f.idx;\n break;\n }\n depth++; // opens an unlabelled nested code block\n } else {\n // A labelled fence opens a nested block; a bare fence at depth > 0 closes\n // the innermost one.\n depth = f.bare ? Math.max(0, depth - 1) : depth + 1;\n }\n nestedCount++;\n maxRun = Math.max(maxRun, f.run);\n }\n\n // Only harden when the block really contains nested fences AND its own closer\n // was found — otherwise leave the text alone rather than extending the block\n // over whatever follows.\n if (closerIdx === -1 || 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 const cm = lines[closerIdx].match(fenceRe)!;\n lines[closerIdx] = `${cm[1]}${fence}`;\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/**\n * The text the panel hands to its markdown renderer. Order matters — image\n * alt-text is flattened before anything else looks at line structure, and the\n * JSON wrap must see the raw payload before fences are hardened.\n */\nexport function prepareMarkdown(content: string): string {\n return hardenNestedCodeFences(normalizeMarkdownTables(wrapBareJson(normalizeImageMarkdown(content))));\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: Translate = (key) => key;\n\n/**\n * Translates `key`, then splices `values` into the result's `{name}` slots.\n *\n * The point is that the key stays a whole sentence. Building a phrase out of\n * translated fragments — \"Waiting for\" + count + \"tasks\" — freezes English word\n * order and English plural rules into the layout, and a translator sees each\n * fragment with no sentence around it. Here the locale owns the whole sentence\n * and decides where the number goes; the host's `t` still does nothing but look\n * a key up, so this needs no support from it.\n *\n * Plurals stay two explicit keys (`'1 tool call'` / `'{count} tool calls'`) for\n * the same reason: a lookup cannot select a plural form, so the call site picks\n * the sentence and the locale translates it whole.\n */\nexport function translate(t: Translate, key: string, values: Record<string, string | number>): string {\n // One pass over the sentence rather than one pass per value: a value that\n // happens to contain `{something}` is then never mistaken for a slot of its\n // own. A slot with no value keeps its braces, so a stray one is visible\n // rather than silently blank.\n return t(key).replace(/\\{(\\w+)\\}/g, (slot, name) => (Object.hasOwn(values, name) ? String(values[name] ?? '') : slot));\n}\n\n/**\n * Same as `translate`, but leaves one slot unfilled and returns the text on\n * either side of it — for the case where the value has to be markup (a coloured\n * agent name, a link) and so cannot be spliced into a string.\n *\n * Without this the sentence gets cut into \"How can \" + node + \" help you, \",\n * which is untranslatable in any language that would not put the node in that\n * spot. Here the locale still owns the whole sentence and decides where the slot\n * sits.\n *\n * `hasSlot` is false for a translation that dropped the slot — a locale that\n * reworded the sentence so it no longer names the value. The caller then renders\n * the sentence without the node instead of letting it dangle off the end.\n */\nexport function translateAround(\n t: Translate,\n key: string,\n slot: string,\n values: Record<string, string | number> = {},\n): { before: string; after: string; hasSlot: boolean } {\n const sentence = translate(t, key, values);\n const token = `{${slot}}`;\n const at = sentence.indexOf(token);\n if (at === -1) return { before: sentence, after: '', hasSlot: false };\n return { before: sentence.slice(0, at), after: sentence.slice(at + token.length), hasSlot: true };\n}\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: Translate): 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 translate(t, '{count}m ago', { count: minutes });\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return translate(t, '{count}h ago', { count: hours });\n const days = Math.floor(hours / 24);\n if (days < 7) return translate(t, '{count}d ago', { count: days });\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\n/**\n * The markdown documents an assistant answer is drawn from, in order: each\n * prose part between its file markers (`splitFileMarkers`), prepared as\n * `MarkdownMessage` prepares it (`prepareMarkdown`). The transcript parses each\n * one as a document of its own, and so must anything that reads the answer\n * (read aloud) to read what the screen shows.\n */\nexport function answerMarkdownSources(content: string): string[] {\n const sources: string[] = [];\n for (const part of splitFileMarkers(content)) {\n if (part.type === 'text' && part.value.trim()) sources.push(prepareMarkdown(part.value));\n }\n return sources;\n}\n","import type { ApiEndpoints, BackendType, ChatMessagePersistedFeedback, MessageFeedback } from '../types';\n\n/**\n * Persisted feedback on an assistant answer: the requests the thumbs send.\n * A stored rating is read back by `parsePersistedFeedback` with the rest of a\n * session restore (`hooks/protocols/parseRestEvent.ts`).\n *\n * Kept free of local runtime imports so `node --test` runs it as is.\n */\n\n/** Longest comment the feedback endpoint stores. */\nexport const FEEDBACK_COMMENT_MAX_LENGTH = 2000;\n\nexport type FeedbackRating = ChatMessagePersistedFeedback['rating'];\n\nexport const ratingOf = (value: MessageFeedback): FeedbackRating => (value === 'up' ? 'positive' : 'negative');\n\nexport const feedbackValueOf = (rating: FeedbackRating): MessageFeedback => (rating === 'positive' ? 'up' : 'down');\n\n/**\n * How the thumbs of one answer work, or null when it shows none:\n * - `persisted`: the panel stores the rating (`ApiEndpoints.feedback`), which\n * needs the conversation and the answer's persisted id - an answer the\n * backend did not identify cannot be rated;\n * - `callback`: the host stores it (`onMessageFeedback` alone).\n */\nexport type FeedbackMode = 'persisted' | 'callback';\n\nexport function feedbackModeOf(\n serverId: string | undefined,\n feedbackUrl: string | null | undefined,\n conversationId: string | null | undefined,\n hasCallback: boolean,\n): FeedbackMode | null {\n if (feedbackUrl) return conversationId && serverId ? 'persisted' : null;\n return hasCallback ? 'callback' : null;\n}\n\n/** The rating shown on an answer, and whether it can still be changed. */\nexport function shownFeedback(\n session: { value: MessageFeedback | null } | undefined,\n stored: ChatMessagePersistedFeedback | undefined,\n): { value: MessageFeedback | null; locked: boolean } {\n // Rated in this session: the session's answer wins, and can be changed.\n if (session) return { value: session.value, locked: false };\n // Rated before the conversation was reopened: shown, and final.\n if (stored) return { value: feedbackValueOf(stored.rating), locked: true };\n return { value: null, locked: false };\n}\n\n/** A comment as the endpoint stores it: trimmed, capped, and null when empty. */\nexport function normalizeFeedbackComment(comment: string | null | undefined): string | null {\n const trimmed = (comment ?? '').trim();\n return trimmed ? trimmed.slice(0, FEEDBACK_COMMENT_MAX_LENGTH) : null;\n}\n\n/**\n * `{apiBaseUrl}{feedback}`, or null when the panel does not store feedback\n * itself: the path is unset (no default, see `ApiEndpoints.feedback`), the\n * backend is not the REST one, or every request goes to one endpoint.\n */\nexport function feedbackBaseUrl(apiBaseUrl: string, apiEndpoints: ApiEndpoints | undefined, backendType: BackendType): string | null {\n if (backendType !== 'rest' || apiEndpoints?.singleEndpoint || !apiEndpoints?.feedback) return null;\n return `${apiBaseUrl}${apiEndpoints.feedback}`;\n}\n\n/**\n * The request that stores (`rating`) or retracts (`null`) the user's rating\n * of one message.\n */\nexport function feedbackRequest(\n baseUrl: string,\n conversationId: string,\n messageId: string,\n rating: { rating: FeedbackRating; comment: string | null } | null,\n requestHeaders?: Record<string, string>,\n): { url: string; init: RequestInit } {\n const url = `${baseUrl}/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/feedback`;\n if (!rating) {\n return { url, init: { method: 'DELETE', credentials: 'include', headers: { ...requestHeaders } } };\n }\n return {\n url,\n init: {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json', ...requestHeaders },\n body: JSON.stringify({ rating: rating.rating, comment: normalizeFeedbackComment(rating.comment) }),\n },\n };\n}\n\n/** The part of `fetch` the feedback request needs. */\nexport type FeedbackFetch = (url: string, init: RequestInit) => Promise<{ ok: boolean }>;\n\n/**\n * Sends one `feedbackRequest` and says whether the backend stored it. A network\n * failure is a refusal like any other status: the caller rolls the thumbs back\n * either way.\n */\nexport async function submitFeedback(\n fetchImpl: FeedbackFetch,\n baseUrl: string,\n conversationId: string,\n messageId: string,\n rating: { rating: FeedbackRating; comment: string | null } | null,\n requestHeaders?: Record<string, string>,\n): Promise<boolean> {\n const { url, init } = feedbackRequest(baseUrl, conversationId, messageId, rating, requestHeaders);\n try {\n return (await fetchImpl(url, init)).ok;\n } catch {\n return false;\n }\n}\n","import type {\n ChatAttachment,\n ChatContextBreakdown,\n ChatContextUsage,\n ChatMessage,\n ChatMessagePersistedFeedback,\n ToolApprovalProposal,\n ToolCallTraceEntry,\n TransferChainEntry,\n} from '../../types';\nimport type { PageAction } from '../../utils/pageActions';\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 // Left empty for the UI to label: a parser has no `t`, and a placeholder\n // baked in here would reach the reviewer untranslated.\n toolName: typeof p.tool_name === 'string' && p.tool_name ? p.tool_name : '',\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/** The page action of an `act_on_page` frame; anything unreadable becomes `unsupported`, never nothing. */\nfunction parsePageAction(raw: unknown): PageAction {\n if (!raw || typeof raw !== 'object') return { type: 'unsupported', requested: 'malformed' };\n const a = raw as Record<string, unknown>;\n const str = (v: unknown) => typeof v === 'string' && v.length > 0;\n if (a.type === 'snapshot') return { type: 'snapshot' };\n if (a.type === 'navigate' && str(a.path)) return { type: 'navigate', path: a.path as string };\n if (a.type === 'click' && str(a.ref)) return { type: 'click', ref: a.ref as string };\n if (a.type === 'open' && str(a.ref)) return { type: 'open', ref: a.ref as string };\n if (a.type === 'type' && str(a.ref) && typeof a.text === 'string') {\n return { type: 'type', ref: a.ref as string, text: a.text };\n }\n // Answered, not dropped: the backend holds the turn open until the action\n // comes back, so an unknown shape would stall until its timeout.\n return { type: 'unsupported', requested: typeof a.type === 'string' ? a.type.slice(0, 64) : 'unknown' };\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_progress' && evt.event === 'page_action_required') {\n // Without an action_id there is nothing to answer against, so that frame\n // alone is dropped; every other shape is answered.\n if (typeof evt.action_id !== 'string' || !evt.action_id) return { action: 'noop' };\n const pageAction = parsePageAction(evt.action);\n return {\n action: 'page_action_required',\n actionId: evt.action_id,\n pageAction,\n conversationId: typeof evt.conversation_id === 'string' ? evt.conversation_id : undefined,\n };\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 messageId: typeof evt.message_id === 'string' && evt.message_id ? evt.message_id : undefined,\n };\n }\n\n return { action: 'noop' };\n}\n\n/** The `feedback` of a restored entry, or undefined for anything that is not a stored rating. */\nexport function parsePersistedFeedback(raw: unknown): ChatMessagePersistedFeedback | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const { rating, comment } = raw as { rating?: unknown; comment?: unknown };\n if (rating !== 'positive' && rating !== 'negative') return undefined;\n return { rating, comment: typeof comment === 'string' && comment.trim() ? comment : null };\n}\n\n/**\n * The messages of a session restore (`POST {sessions}` with a known\n * `conversation_id`), in order.\n *\n * Rows are keyed `restored-<index>`: the key only has to be stable for the\n * life of the transcript, and a restore replaces the transcript whole. The\n * entry's own `id` is the message's persisted id, kept as `serverId` for what\n * is stored against it (feedback); its `created_at` is when it was sent, and\n * a backend that sends neither still restores - with no time to show\n * (`timestampUnknown`) rather than the time of the restore.\n * Entries that are not a user or an assistant message are skipped.\n */\nexport function parseRestoredMessages(raw: unknown): ChatMessage[] {\n if (!Array.isArray(raw)) return [];\n const out: ChatMessage[] = [];\n raw.forEach((item: unknown, i: number) => {\n if (!item || typeof item !== 'object') return;\n const m = item as Record<string, unknown>;\n if (m.role !== 'user' && m.role !== 'assistant') return;\n const createdAt = typeof m.created_at === 'string' ? new Date(m.created_at) : null;\n const sentAt = createdAt && !Number.isNaN(createdAt.getTime()) ? createdAt : null;\n out.push({\n id: `restored-${i}`,\n serverId: typeof m.id === 'string' && m.id ? m.id : undefined,\n role: m.role,\n content: typeof m.content === 'string' ? m.content : '',\n timestamp: sentAt ?? new Date(),\n timestampUnknown: sentAt ? undefined : true,\n feedback: m.role === 'assistant' ? parsePersistedFeedback(m.feedback) : undefined,\n // Per-message attribution when the backend keeps it — the only way a\n // thread that changed hands mid-way reads correctly. Nothing records it\n // today, so this is normally undefined and the conversation's agent\n // applies to the whole thread.\n agentName: typeof m.agent_name === 'string' ? m.agent_name : undefined,\n // Downloadable file chips on both roles: agent-generated deliverables on\n // assistant messages (their [[FILE:…]] markers are stripped at render\n // time) and user uploads, so an upload stays downloadable after a reload\n // and not only in the live session where it is carried on `files`.\n attachments: parseAttachments(m.attachments),\n // The reasoning-details affordance, from the 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 return out;\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 // Empty rather than a literal English fallback: the content is rendered as\n // the assistant's message, and the consumer supplies its own translated\n // text when the backend sends none — as the other protocols already do.\n return { action: 'error', content: typeof evt.message === 'string' ? evt.message : '' };\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","/**\n * Performing one `act_on_page` action on the host page (#5559). Navigation goes\n * through the host's router, never a page load, which would end the chat turn —\n * so a link is followed by routing its href, not by clicking it.\n */\n\nexport type PageAction =\n | { type: 'snapshot' }\n | { type: 'navigate'; path: string }\n | { type: 'click'; ref: string }\n | { type: 'type'; ref: string; text: string }\n | { type: 'open'; ref: string }\n /** A frame naming an action this version does not know (backend ahead of the panel). */\n | { type: 'unsupported'; requested: string };\n\n/**\n * Whether the panel may declare `supports_page_actions` (#5559). Passing\n * `onRelativeLinkClick` is the host's opt-in: a proxy that relays tool\n * approvals but not page answers would stall every step on its timeout.\n */\nexport function hostTakesPageActions(approveUrl: string | null, onNavigate?: (path: string) => void): boolean {\n return approveUrl !== null && typeof onNavigate === 'function';\n}\n\n/** The document's own address, which every href is resolved against. */\nfunction baseHref(doc?: Document): string | undefined {\n return doc?.defaultView?.location?.href ?? (typeof window === 'undefined' ? undefined : window.location.href);\n}\n\n/**\n * The host-internal form of *href*, or null when it leads anywhere else.\n * Resolved and origin-compared even when it looks relative: `/\\evil.com` and\n * ` //evil.com` do not, and resolve to another origin.\n */\nexport const toInternalHref = (href?: string, base?: string): string | null => {\n const from = base ?? baseHref();\n if (!href || !from) return null;\n try {\n const url = new URL(href, from);\n if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;\n if (url.origin !== new URL(from).origin) return null;\n const path = `${url.pathname}${url.search}${url.hash}` || '/';\n // `/.//host/x` normalises to `//host/x`, which passes the origin check here\n // and then reads as protocol-relative to a host that resolves it again.\n return path.startsWith('//') ? null : path;\n } catch {\n return null;\n }\n};\n\n/**\n * Whether *href* is *written* as a relative reference. Not a safety boundary -\n * `toInternalHref` is - but the shape a link was authored in: a same-origin\n * absolute link still opens in a new tab when the host routes nothing.\n */\nexport const isRelativeHref = (href?: string): boolean => {\n if (!href) return false;\n if (href.startsWith('//')) return false;\n return !/^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(href);\n};\n\nexport interface PageActionResult {\n ok: boolean;\n error?: string;\n}\n\n/** The panel's own subtree, whose mutations must never count as the page reacting. */\nconst PANEL_SELECTOR = '.filigran-chatbot';\n\n/** How long a hover is given to open a menu. */\nconst HOVER_WAIT_MS = 300;\n\nfunction insidePanel(node: Node | null): boolean {\n // Duck-typed: `instanceof Element` throws where there is no such global, and\n // the observer swallows the throw, losing the mutation.\n const el = (node && 'closest' in node ? node : node?.parentElement) as Element | null | undefined;\n return !!el?.closest?.(PANEL_SELECTOR);\n}\n\n/**\n * Fields `type` may write to. A `<select>`, link or button would otherwise fall\n * through to `textContent` and lose its options or its children. Password is\n * left out on purpose: the panel neither reads nor fills credentials.\n */\nconst TYPEABLE =\n 'textarea, [contenteditable=\"true\"], input:not([type]), input[type=\"text\"], input[type=\"search\"], input[type=\"email\"], input[type=\"url\"], input[type=\"tel\"], input[type=\"number\"]';\n\nfunction isTypeable(el: Element): boolean {\n return el.matches?.(TYPEABLE) ?? false;\n}\n\n/**\n * Secret fields, by autocomplete. Mirrors `pageSnapshot`'s own test, which\n * cannot be shared: both modules are loaded by `node --test`, where a runtime\n * relative import does not resolve. `secretFieldsAgree` in the tests pins them\n * to the same answer.\n */\nconst SECRET_AUTOCOMPLETE = /^(current-password|new-password|one-time-code|cc-)/;\n\nexport function isSecretField(el: Element): boolean {\n if ((el.getAttribute('type') || '').toLowerCase() === 'password') return true;\n return (el.getAttribute('autocomplete') || '')\n .toLowerCase()\n .split(/\\s+/)\n .some((token) => SECRET_AUTOCOMPLETE.test(token));\n}\n\n/** Set *text* the way a user's typing does, so a React-controlled field sees it. */\nfunction setValue(el: Element, text: string): void {\n const view = el.ownerDocument.defaultView as (Window & typeof globalThis) | null;\n if (view && (el instanceof view.HTMLInputElement || el instanceof view.HTMLTextAreaElement)) {\n // React tracks the value it last rendered on the element itself; only the\n // prototype's setter goes around that tracker, so the input event below\n // reaches the component as a change.\n const proto = el instanceof view.HTMLInputElement ? view.HTMLInputElement.prototype : view.HTMLTextAreaElement.prototype;\n Object.getOwnPropertyDescriptor(proto, 'value')?.set?.call(el, text);\n } else {\n el.textContent = text;\n }\n el.dispatchEvent(new (view?.Event ?? Event)('input', { bubbles: true }));\n el.dispatchEvent(new (view?.Event ?? Event)('change', { bubbles: true }));\n}\n\n/** Hover *el* the way a pointer does: what opens a menu on hover opens. */\nfunction hover(el: Element): void {\n const view = el.ownerDocument.defaultView as (Window & typeof globalThis) | null;\n const init = { bubbles: true, cancelable: true, view: view ?? undefined, pointerType: 'mouse' };\n const Pointer = view?.PointerEvent ?? view?.MouseEvent ?? MouseEvent;\n const Mouse = view?.MouseEvent ?? MouseEvent;\n el.dispatchEvent(new Pointer('pointerover', init));\n el.dispatchEvent(new Pointer('pointerenter', { ...init, bubbles: false }));\n el.dispatchEvent(new Mouse('mouseover', init));\n el.dispatchEvent(new Mouse('mouseenter', { ...init, bubbles: false }));\n}\n\n/**\n * Whether *act* adds anything to the page within *ms*: a menu that opened.\n * The panel's own re-render (the \"Acting on the page…\" status) lands inside the\n * same window, so its subtree is ignored or every hover would look successful.\n */\nfunction addsContent(doc: Document, act: () => void, ms = HOVER_WAIT_MS): Promise<boolean> {\n const view = doc.defaultView as (Window & typeof globalThis) | null;\n const Observer = view?.MutationObserver ?? globalThis.MutationObserver;\n if (!Observer) {\n act();\n return Promise.resolve(true);\n }\n return new Promise((resolve) => {\n const observer = new Observer((records) => {\n const added = records.some(\n (r) => r.addedNodes.length > 0 && !insidePanel(r.target) && Array.from(r.addedNodes).some((n) => !insidePanel(n)),\n );\n if (!added) return;\n observer.disconnect();\n clearTimeout(timer);\n resolve(true);\n });\n observer.observe(doc.body ?? doc.documentElement, { subtree: true, childList: true });\n act();\n const timer = setTimeout(() => {\n observer.disconnect();\n resolve(false);\n }, ms);\n });\n}\n\n/**\n * Run *act*, cancelling the default action of whatever event it fires. Capture\n * phase, so handlers still run: the panel may activate a control but must never\n * cause the page load that would end the turn.\n */\nfunction withoutDefault(doc: Document, types: string[], act: () => void): void {\n const suppress = (event: Event) => event.preventDefault();\n for (const type of types) doc.addEventListener(type, suppress, true);\n try {\n act();\n } finally {\n for (const type of types) doc.removeEventListener(type, suppress, true);\n }\n}\n\n/**\n * Click *el*, routing an anchor through the host so the page is not left and\n * the turn ended. Only *el* itself: a control inside a link row has its own\n * handler, which routing the row would skip.\n */\nfunction clickOrRoute(el: Element, onNavigate?: (path: string) => void, base?: string): PageActionResult {\n if (el.matches?.('a[href]')) {\n // Before the fragment case below: `<a href=\"#\" target=\"_blank\">` still\n // opens a tab, which leaves the page the turn is acting on.\n const target = el.getAttribute('target');\n if (target && target !== '_self') return { ok: false, error: 'opens_new_tab' };\n // A fragment is an in-page trigger carrying a handler, not a destination,\n // so it is clicked like anything else.\n const href = el.getAttribute('href') ?? '';\n if (href && !href.startsWith('#')) {\n const internal = toInternalHref(href, base);\n if (!internal) return { ok: false, error: 'external_link' };\n if (onNavigate) {\n onNavigate(internal);\n return { ok: true };\n }\n }\n }\n // Focus first: a click moves no focus of its own, so a field edited just\n // before would never blur, and a host that commits on blur (a tag input)\n // would run this activation without the edit.\n (el as HTMLElement).focus?.();\n // A submit control loads its form's action, and a click anywhere inside an\n // anchor activates it - both end the turn. Their handlers still run.\n // A fragment moves within the page, so its default stands; anything else an\n // anchor does loads one.\n const anchor = el.closest?.('a[href]');\n const inAnchor = !!anchor && !(anchor.getAttribute('href') ?? '').startsWith('#');\n const cancels = inAnchor ? ['submit', 'click'] : ['submit'];\n const checkable = el.matches?.('input[type=\"checkbox\"], input[type=\"radio\"]') ?? false;\n const before = (el as HTMLInputElement).checked;\n withoutDefault(el.ownerDocument, cancels, () => (el as HTMLElement).click());\n // Cancelling the click also rolled back a checkable's own activation, which\n // is its behaviour rather than the anchor's: applied here instead.\n if (checkable && inAnchor && (el as HTMLInputElement).checked === before) {\n const input = el as HTMLInputElement;\n input.checked = input.type === 'radio' ? true : !before;\n input.dispatchEvent(new (el.ownerDocument.defaultView?.Event ?? Event)('input', { bubbles: true }));\n input.dispatchEvent(new (el.ownerDocument.defaultView?.Event ?? Event)('change', { bubbles: true }));\n }\n return { ok: true };\n}\n\n/** Resolve once *test* holds, or after *ms*. */\nfunction waitUntil(test: () => boolean, ms: number): Promise<boolean> {\n return new Promise((resolve) => {\n const started = Date.now();\n const tick = () => {\n if (test()) return resolve(true);\n if (Date.now() - started >= ms) return resolve(false);\n setTimeout(tick, 30);\n };\n tick();\n });\n}\n\n/** Whether *el* and every ancestor are rendered: a wrapper hides what it holds. */\nfunction visible(el: Element): boolean {\n const details = el.closest?.('details:not([open])');\n if (details && details !== el) {\n const summary = Array.from(details.children).find((c) => c.tagName.toLowerCase() === 'summary');\n if (!summary?.contains(el)) return false;\n }\n for (let node: Element | null = el; node; node = node.parentElement) {\n if ((node as HTMLElement).hidden || node.hasAttribute('inert')) return false;\n if (node.getAttribute('aria-hidden') === 'true') return false;\n const style = node.ownerDocument.defaultView?.getComputedStyle(node);\n if (style?.display === 'none' || style?.visibility === 'hidden') return false;\n }\n return true;\n}\n\n/** Whether the menu *el* controls is showing, by what the trigger says about it. */\nfunction menuShowing(el: Element): boolean {\n const expanded = el.getAttribute('aria-expanded');\n if (expanded === 'true') return true;\n // A stated collapsed state settles it, whatever the target looks like.\n if (expanded === 'false') return false;\n const id = el.getAttribute('aria-controls');\n if (!id) return false;\n const menu = el.ownerDocument.getElementById(id);\n return !!menu && visible(menu);\n}\n\n/**\n * Open the menu *el* triggers: by hover where that opens it, else by click.\n * A trigger stating its own state is believed; only one that says nothing\n * falls back to \"did the page grow\", which a tooltip would answer for it.\n */\nasync function openMenu(el: Element, base?: string, signal?: AbortSignal): Promise<PageActionResult> {\n const statesItself = el.hasAttribute('aria-expanded') || el.hasAttribute('aria-controls');\n if (menuShowing(el)) return { ok: true };\n if (statesItself) {\n // Waited on for its own state: anything else the hover adds, a tooltip,\n // says nothing about the menu and would cut the wait short.\n hover(el);\n if (await waitUntil(() => menuShowing(el), HOVER_WAIT_MS)) return { ok: true };\n } else if (await addsContent(el.ownerDocument, () => hover(el))) {\n return { ok: true };\n }\n // The turn may have been stopped during the hover wait; the click below would\n // land on the user's page after they asked for it to end.\n if (signal?.aborted) return { ok: false, error: 'aborted' };\n // No router: a trigger that happens to be an anchor must run its own click\n // handler rather than navigate to its href. The link guards still apply.\n return clickOrRoute(el, undefined, base);\n}\n\n/** Everything in a form a user fills in, for telling a search from a dialog. */\nconst SUBMITTABLE =\n 'input:not([type=\"hidden\"]):not([type=\"submit\"]):not([type=\"button\"]):not([type=\"reset\"]):not([type=\"image\"]), select, textarea, [contenteditable=\"true\"]';\n\n/** Words a search field is labelled with, where a create dialog's is not. */\nconst SEARCH_WORDS = /\\b(search|query|filter|find|recherche|rechercher)\\b/i;\n\n/**\n * Whether Enter in *el* asks for what was typed. Only a field that says it is a\n * search: a form of one field is just as likely a create or rename dialog, and\n * submitting it would commit the change the user has not asked for yet.\n */\nfunction entersSearch(el: Element): boolean {\n if (el.getAttribute('role') === 'searchbox') return true;\n if ((el as HTMLInputElement).type === 'search') return true;\n if (el.closest('[role=\"search\"]') !== null) return true;\n if (!['name', 'id', 'placeholder', 'aria-label', 'title'].some((attr) => SEARCH_WORDS.test(el.getAttribute(attr) ?? ''))) return false;\n // Named like a search, but a \"Filter name\" beside other fields is a dialog\n // saving a filter, not a search: only the form's sole field is sent.\n const form = (el as HTMLInputElement).form;\n return !form || form.querySelectorAll(SUBMITTABLE).length === 1;\n}\n\nfunction pressEnter(el: Element): void {\n const view = el.ownerDocument.defaultView as (Window & typeof globalThis) | null;\n const Keyboard = view?.KeyboardEvent ?? KeyboardEvent;\n const init = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };\n const down = new Keyboard('keydown', init);\n el.dispatchEvent(down);\n el.dispatchEvent(new Keyboard('keyup', init));\n // A synthetic key event never triggers the browser's implicit submission, so\n // a form the field belongs to is submitted unless a handler took the key.\n // Its native submission is cancelled: `requestSubmit` still validates and\n // still reaches the host's own handler, but cannot unload the page.\n const form = (el as HTMLInputElement).form;\n if (form && !down.defaultPrevented) {\n withoutDefault(el.ownerDocument, ['submit'], () => form.requestSubmit?.());\n }\n}\n\n/**\n * Mirrors `pageSnapshot`'s own test, which cannot be shared: both modules are\n * loaded by `node --test`, where a runtime relative import does not resolve.\n * `disabledTestsAgree` in the tests pins them to the same answer.\n */\nexport function isDisabled(el: Element): boolean {\n // The attribute as authored, so what the snapshot prints `(disabled)` for is\n // what is refused here - a custom `<a disabled>` included.\n if (el.hasAttribute('disabled')) return true;\n if (el.getAttribute('aria-disabled') === 'true') return true;\n if (el.matches?.(':disabled')) return true;\n // Only the native form controls a `<fieldset>` governs: a link or an ARIA\n // widget inside a disabled one still works. Its first `<legend>` is exempt.\n if (!el.matches?.('button, input, select, textarea')) return false;\n const fieldset = el.closest?.('fieldset[disabled]');\n if (!fieldset) return false;\n const legend = Array.from(fieldset.children).find((c) => c.tagName.toLowerCase() === 'legend');\n return !legend?.contains(el);\n}\n\n/** A field the user cannot edit either: the native setter would still write to it. */\nfunction isReadOnly(el: Element): boolean {\n return (el as HTMLInputElement).readOnly === true || el.getAttribute('aria-readonly') === 'true';\n}\n\nexport async function performPageAction(\n action: PageAction,\n refs: Map<string, Element>,\n onNavigate?: (path: string) => void,\n doc?: Document,\n signal?: AbortSignal,\n): Promise<PageActionResult> {\n try {\n if (signal?.aborted) return { ok: false, error: 'aborted' };\n if (action.type === 'snapshot') return { ok: true };\n if (action.type === 'unsupported') return { ok: false, error: `unsupported_action: ${action.requested}`.slice(0, 500) };\n if (action.type === 'navigate') {\n if (!onNavigate) return { ok: false, error: 'navigation_unavailable' };\n const path = toInternalHref(action.path, baseHref(doc));\n if (!path) return { ok: false, error: 'external_path' };\n onNavigate(path);\n return { ok: true };\n }\n const el = refs.get(action.ref);\n if (!el || !el.isConnected) return { ok: false, error: 'unknown_ref' };\n // Still in the document is not still reachable: a dialog may have closed or\n // an ancestor gone inert since the snapshot named it.\n if (!visible(el)) return { ok: false, error: 'not_visible' };\n if (isDisabled(el)) return { ok: false, error: 'disabled' };\n // The file chooser is an operating-system window: it cannot be driven from\n // here, and the turn would wait behind it.\n if (el.matches?.('input[type=\"file\"]')) return { ok: false, error: 'opens_file_dialog' };\n if (action.type === 'open') return await openMenu(el, baseHref(doc ?? el.ownerDocument), signal);\n if (action.type === 'click') return clickOrRoute(el, onNavigate, baseHref(doc ?? el.ownerDocument));\n if (!isTypeable(el)) return { ok: false, error: 'not_a_text_field' };\n if (isReadOnly(el)) return { ok: false, error: 'read_only' };\n // A revealed password or one-time code is masked in the snapshot; writing\n // to it would be filling a credential the panel will not read back.\n if (isSecretField(el)) return { ok: false, error: 'secret_field' };\n (el as HTMLElement).focus?.();\n setValue(el, action.text);\n if (entersSearch(el)) pressEnter(el);\n return { ok: true };\n } catch (err) {\n return { ok: false, error: `failed: ${err instanceof Error ? err.message : String(err)}`.slice(0, 500) };\n }\n}\n\n/**\n * Resolve once the page has not changed for *quietMs*, or after *maxMs* at most.\n * The panel's own subtree is ignored: its status re-renders would otherwise keep\n * resetting the quiet timer until the cap.\n */\nexport function waitForDomSettle(\n doc: Document,\n { quietMs = 300, maxMs = 3000, signal }: { quietMs?: number; maxMs?: number; signal?: AbortSignal } = {},\n): Promise<void> {\n return new Promise((resolve) => {\n // Before anything is set up: a stopped turn has nothing to wait for.\n if (signal?.aborted) return resolve();\n const view = doc.defaultView as (Window & typeof globalThis) | null;\n const Observer = view?.MutationObserver ?? globalThis.MutationObserver;\n let quiet: ReturnType<typeof setTimeout>;\n const done = () => {\n clearTimeout(quiet);\n clearTimeout(cap);\n observer?.disconnect();\n signal?.removeEventListener('abort', done);\n resolve();\n };\n const observer = Observer\n ? new Observer((records) => {\n if (!records.some((r) => !insidePanel(r.target))) return;\n clearTimeout(quiet);\n quiet = setTimeout(done, quietMs);\n })\n : null;\n observer?.observe(doc.body ?? doc.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });\n quiet = setTimeout(done, quietMs);\n const cap = setTimeout(done, maxMs);\n signal?.addEventListener('abort', done);\n });\n}\n","/**\n * The host page as `act_on_page` reads it (#5559): one line per element a user\n * could act on, keyed by a reference the next action names it by. The panel\n * itself is never part of it. The line format is in the README, under \"Acting\n * on the host page\".\n */\n\nexport const MAX_SNAPSHOT_CHARS = 20_000;\nconst MAX_NAME_CHARS = 80;\nconst MAX_URL_CHARS = 500;\nconst TRUNCATION_MARKER = '[truncated]';\n\nexport interface PageSnapshot {\n text: string;\n url: string;\n title: string;\n /** Reference → element, valid until the next snapshot. */\n refs: Map<string, Element>;\n}\n\nconst EXPLICIT_ROLES = new Set([\n 'button',\n 'link',\n 'tab',\n 'menuitem',\n 'menuitemcheckbox',\n 'menuitemradio',\n 'option',\n 'checkbox',\n 'radio',\n 'switch',\n 'textbox',\n 'searchbox',\n 'combobox',\n 'treeitem',\n]);\n\n/** Anything that could carry an actionable role, for the descent test below. */\nconst MAYBE_ACTIONABLE = 'a[href], button, summary, select, textarea, input, [contenteditable=\"true\"], [role]';\n\n/**\n * Whether an actionable element wraps further controls, which need references\n * of their own. A plain link is named by its contents; a `<select>`'s are not\n * controls.\n */\nfunction wrapsControls(el: Element): boolean {\n if (el.tagName.toLowerCase() === 'select') return false;\n for (const child of Array.from(el.querySelectorAll(MAYBE_ACTIONABLE))) {\n if (actionableRole(child)) return true;\n }\n return false;\n}\n\n/** The role an element is acted on as, or `null` when it is not actionable. */\nfunction actionableRole(el: Element): string | null {\n const explicit = el.getAttribute('role');\n if (explicit && EXPLICIT_ROLES.has(explicit)) return explicit;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' && el.hasAttribute('href')) return 'link';\n if (tag === 'button' || tag === 'summary') return 'button';\n if (tag === 'select') return 'combobox';\n if (tag === 'textarea') return 'textbox';\n if (tag === 'input') {\n const type = (el.getAttribute('type') || 'text').toLowerCase();\n if (type === 'hidden') return null;\n if (['button', 'submit', 'reset', 'image'].includes(type)) return 'button';\n if (type === 'checkbox' || type === 'radio') return type;\n if (type === 'search') return 'searchbox';\n // Opens a chooser rather than taking text; calling it a textbox invites a\n // `type` that is refused anyway.\n if (type === 'file') return 'button';\n return 'textbox';\n }\n if (el.getAttribute('contenteditable') === 'true') return 'textbox';\n return null;\n}\n\nfunction headingLevel(el: Element): number | null {\n const m = /^h([1-6])$/i.exec(el.tagName);\n return m ? Number(m[1]) : null;\n}\n\n/**\n * The element's text with a space wherever a non-inline element starts or\n * ends. `textContent` glues a table row's cells together (\"AdministratorZone de\n * danger1\"), and a name whose first cell is not a word cannot be matched.\n */\nfunction spacedText(el: Element): string {\n const view = el.ownerDocument.defaultView;\n let out = '';\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === 3) {\n out += node.textContent ?? '';\n } else if (node.nodeType === 1) {\n // The walk skips hidden subtrees, and a secret's value is masked; without\n // the same tests here both return as part of an ancestor's name, its\n // value, or a heading above them.\n if (hidden(node as Element) || isSecret(node as Element)) continue;\n const display = view?.getComputedStyle(node as Element).display ?? '';\n // An unknown display (no layout engine) is read as inline: never split a word.\n const inline = display === '' || display === 'inline' || display === 'contents';\n out += inline ? spacedText(node as Element) : ` ${spacedText(node as Element)} `;\n }\n }\n return out;\n}\n\nfunction clean(text: string | null | undefined): string {\n return (text ?? '').replace(/\\s+/g, ' ').trim();\n}\n\n/** Roles named by what they contain, where `title` is only a last resort. */\nconst NAMED_BY_CONTENT = new Set(['button', 'link', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'treeitem', 'switch']);\n\n/** The name a user reads for *el*, in the accessible-name order. */\nfunction accessibleName(el: Element, role: string): string {\n const doc = el.ownerDocument;\n // `aria-labelledby` outranks `aria-label`, and is read even when hidden: a\n // visually-hidden span is how an icon button usually gets its name.\n const labelledBy = el.getAttribute('aria-labelledby');\n if (labelledBy) {\n const text = clean(\n labelledBy\n .split(/\\s+/)\n .map((id) => doc.getElementById(id)?.textContent ?? '')\n .join(' '),\n );\n if (text) return text;\n }\n const fromAttr = clean(el.getAttribute('aria-label'));\n if (fromAttr) return fromAttr;\n const id = el.getAttribute('id');\n const label = (id && Array.from(doc.getElementsByTagName('label')).find((l) => l.htmlFor === id)) || el.closest('label');\n if (label) {\n // The label's own words, not the options of a select it wraps.\n const own = clean(\n Array.from(label.childNodes)\n .filter((n) => n.nodeType === 3)\n .map((n) => n.textContent)\n .join(' '),\n );\n if (own || clean(label.textContent)) return own || clean(label.textContent);\n }\n // An `<input>` button carries its label in `value`, not in any content.\n const tag = el.tagName.toLowerCase();\n const type = (el.getAttribute('type') || '').toLowerCase();\n if (tag === 'input' && ['submit', 'reset', 'button'].includes(type)) {\n const value = clean(el.getAttribute('value'));\n if (value) return value;\n if (type !== 'button') return type === 'submit' ? 'Submit' : 'Reset';\n }\n const alt = clean(el.getAttribute('alt')) || clean(el.querySelector('img[alt]')?.getAttribute('alt'));\n if (alt) return alt;\n // Its contents would hand over what `value()` just masked.\n const contents = isSecret(el) ? '' : clean(spacedText(el));\n // A control named by what it shows: the visible words beat a `title`, which\n // often holds a shortcut rather than a label.\n if (NAMED_BY_CONTENT.has(role) && contents) return contents;\n const placeholder = clean(el.getAttribute('placeholder'));\n if (placeholder) return placeholder;\n const title = clean(el.getAttribute('title'));\n if (title) return title;\n return contents;\n}\n\nfunction quote(text: string): string {\n // JSON quoting, so a newline in a textarea value cannot split one control\n // across lines or make its text read as another `[e2] button \"…\"` row.\n return JSON.stringify(text.slice(0, MAX_NAME_CHARS));\n}\n\n/**\n * Whether an ancestor `<fieldset disabled>` disables *el*. Only the native form\n * controls it governs - a link or an ARIA widget inside one still works - and\n * never what sits in its first `<legend>`.\n */\nfunction disabledByFieldset(el: Element): boolean {\n if (!el.matches?.('button, input, select, textarea')) return false;\n const fieldset = el.closest?.('fieldset[disabled]');\n if (!fieldset) return false;\n const legend = Array.from(fieldset.children).find((c) => c.tagName.toLowerCase() === 'legend');\n return !legend?.contains(el);\n}\n\nfunction states(el: Element): string[] {\n const out: string[] = [];\n if (el.hasAttribute('disabled') || disabledByFieldset(el) || el.getAttribute('aria-disabled') === 'true') out.push('disabled');\n if (el.getAttribute('aria-selected') === 'true') out.push('selected');\n if ((el as HTMLInputElement).checked || el.getAttribute('aria-checked') === 'true') out.push('checked');\n if (el.getAttribute('aria-expanded') === 'true') out.push('expanded');\n // A menu that opens on hover or click and is closed now: its items are not\n // in the page until it opens (`open` action), so the snapshot says it is there.\n else if ((el.getAttribute('aria-haspopup') ?? 'false') !== 'false') out.push('menu');\n // A closed disclosure (a collapsed navigation group): what it holds appears\n // once it is clicked, which its name alone does not say.\n else if (el.getAttribute('aria-expanded') === 'false') out.push('collapsed');\n if (el.getAttribute('aria-pressed') === 'true') out.push('pressed');\n const current = el.getAttribute('aria-current');\n if (current && current !== 'false') out.push('current');\n return out;\n}\n\n/**\n * Secret fields, by autocomplete: `type=password` alone misses a revealed\n * password, card fields and one-time codes.\n */\nconst SECRET_AUTOCOMPLETE = /^(current-password|new-password|one-time-code|cc-)/;\n\nfunction isSecret(el: Element): boolean {\n if ((el.getAttribute('type') || '').toLowerCase() === 'password') return true;\n // The attribute may carry section/billing tokens before the field name.\n return (el.getAttribute('autocomplete') || '')\n .toLowerCase()\n .split(/\\s+/)\n .some((token) => SECRET_AUTOCOMPLETE.test(token));\n}\n\nfunction value(el: Element, role: string): string | null {\n if (!['textbox', 'searchbox', 'combobox'].includes(role)) return null;\n if (isSecret(el)) return null;\n // `spacedText`, not `textContent`: a contenteditable's hidden descendants\n // are excluded from the walk and its name, and must not return as its value.\n const v = 'value' in el ? String((el as HTMLInputElement).value ?? '') : clean(spacedText(el));\n return v ? v : null;\n}\n\n/**\n * Where an element with an address within the host goes (` -> /path?query`), so a link named for a\n * section (\"Analyses\") still says it leads to the page asked for. Nothing for a\n * link to another site: its address would only be text the host never shows.\n */\nfunction destination(el: Element): string {\n const href = el.getAttribute('href');\n const loc = el.ownerDocument.defaultView?.location;\n if (!href || !loc) return '';\n try {\n const url = new URL(href, loc.href);\n return url.origin === loc.origin ? ` -> ${url.pathname}${url.search}` : '';\n } catch {\n return '';\n }\n}\n\nfunction hidden(el: Element): boolean {\n if (el.classList.contains('filigran-chatbot')) return true;\n // A closed `<details>` renders nothing but its first `<summary>`, though its\n // descendants keep ordinary computed styles.\n const details = el.closest?.('details:not([open])');\n if (details && details !== el) {\n const summary = Array.from(details.children).find((c) => c.tagName.toLowerCase() === 'summary');\n if (!summary?.contains(el)) return true;\n }\n if ((el as HTMLElement).hidden || el.hasAttribute('inert')) return true;\n if (el.getAttribute('aria-hidden') === 'true') return true;\n const style = el.ownerDocument.defaultView?.getComputedStyle(el);\n return style?.display === 'none' || style?.visibility === 'hidden';\n}\n\nexport function takePageSnapshot(doc: Document, maxChars = MAX_SNAPSHOT_CHARS): PageSnapshot {\n const loc = doc.defaultView?.location;\n const url = (loc ? `${loc.pathname}${loc.search}` : '').slice(0, MAX_URL_CHARS);\n const title = clean(doc.title).slice(0, MAX_NAME_CHARS);\n // Without a marker the agent reads a list cut off at the cap as the whole\n // page, so its room is taken out of the budget before anything is listed -\n // as is the header's, which a long title and path could otherwise blow on\n // their own.\n const budget = Math.max(0, maxChars - (TRUNCATION_MARKER.length + 1));\n let truncated = false;\n let header = `# ${title} — ${url}`;\n if (header.length > budget) {\n header = header.slice(0, budget);\n truncated = true;\n }\n const lines = [header];\n let size = header.length;\n const refs = new Map<string, Element>();\n\n const walk = (node: Element): boolean => {\n for (const el of Array.from(node.children)) {\n if (hidden(el)) continue;\n const role = actionableRole(el);\n const level = headingLevel(el);\n let line: string | null = null;\n if (role) {\n const ref = `e${refs.size + 1}`;\n const v = value(el, role);\n const st = states(el);\n line = `[${ref}] ${role} ${quote(accessibleName(el, role))}${st.length ? ` (${st.join(', ')})` : ''}${v ? ` = ${quote(v)}` : ''}${destination(el)}`;\n if (size + line.length + 1 > budget) {\n truncated = true;\n return false;\n }\n refs.set(ref, el);\n } else if (level) {\n line = `heading ${quote(clean(spacedText(el)))} (level ${level})`;\n if (size + line.length + 1 > budget) {\n truncated = true;\n return false;\n }\n }\n if (line) {\n lines.push(line);\n size += line.length + 1;\n }\n // An actionable element's own content is its name, not more elements -\n // unless it wraps controls of its own, which need their own references.\n if ((!role || wrapsControls(el)) && !walk(el)) return false;\n }\n return true;\n };\n if (doc.body && !truncated) walk(doc.body);\n if (truncated) lines.push(TRUNCATION_MARKER);\n\n return { text: lines.join('\\n'), url, title, refs };\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';\nimport { createStreamDeltas, type StreamDeltas } from './streamDeltas';\nimport { hostTakesPageActions, performPageAction, waitForDomSettle, type PageAction } from '../utils/pageActions';\nimport { takePageSnapshot } from '../utils/pageSnapshot';\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 /** The host's in-app navigation, which `act_on_page` uses to open a path (#5559). */\n onRelativeLinkClick?: (href: string) => void;\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 /** Same promise, for `act_on_page` frames (#5559): answered on the approve path. */\n supportsPageActions?: 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 if (opts.supportsPageActions) {\n body.supports_page_actions = 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 onRelativeLinkClick,\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 // Page actions abort on their own: closing the panel must stop them touching\n // the host page without cancelling the turn, whose answer a host expects to\n // find in history when the panel reopens.\n const pageActionControllerRef = useRef<AbortController | null>(null);\n // The running stream's pending deltas, so stopping it can apply them and a\n // new chat can drop them.\n const streamDeltasRef = useRef<StreamDeltas | 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 // The elements of the last host-page snapshot, by the reference the next\n // `act_on_page` action names them with (#5559).\n const pageRefsRef = useRef<Map<string, Element>>(new Map());\n const onRelativeLinkClickRef = useRef(onRelativeLinkClick);\n onRelativeLinkClickRef.current = onRelativeLinkClick;\n // A page action answers from the closure of the render that began the turn,\n // so a header rotated since would go out stale and 401 the answer.\n const requestHeadersRef = useRef(requestHeaders);\n requestHeadersRef.current = requestHeaders;\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 // Same reason as `requestHeadersRef`: the answer must resolve the route\n // against the endpoints as they are now, not as they were at turn start.\n const getApproveUrlRef = useRef(getApproveUrl);\n getApproveUrlRef.current = getApproveUrl;\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 ? t('This turn is no longer waiting for a decision.') : 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 /**\n * Perform one `act_on_page` action on the host page and answer it with a\n * fresh snapshot (#5559). The answer goes to the approve path, which the host\n * proxies already forward; the turn waits for it on the open stream.\n *\n * A 409 means the turn no longer waits for this action (it timed out or\n * ended): nothing to retry, so it is dropped.\n */\n const answerPageAction = async (actionId: string, action: PageAction, conversationIdHint?: string) => {\n const approveUrl = getApproveUrlRef.current();\n const convId = conversationIdHint ?? conversationIdRef.current;\n if (!approveUrl || !convId || typeof document === 'undefined') return;\n // The turn's own signal: Stop, New chat and switching conversation abort it,\n // and this runs detached from the stream, so without it a stopped turn would\n // still click the page and then answer for a conversation that is gone.\n const signal = pageActionControllerRef.current?.signal;\n // The stream outlives a closed panel, so the turn is still waiting: it is\n // answered without the page and without the aborted signal, which would\n // cancel the request too and leave it waiting out its timeout per step.\n const refuse = async (error: string) => {\n try {\n await fetch(approveUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeadersRef.current ?? {}) },\n body: JSON.stringify({\n conversation_id: convId,\n page_action: { action_id: actionId, ok: false, error, url: '', title: '', snapshot: '' },\n }),\n });\n } catch {\n // The turn's own page wait ends it; there is nothing to show here.\n }\n };\n if (signal?.aborted) return refuse('panel_closed');\n // The same opt-in that decides whether the capability is advertised, tested\n // again on the way in: an unsolicited frame must not read or act on the page\n // of a host that never offered it. Answered, so the turn does not hang.\n if (!hostTakesPageActions(approveUrl, onRelativeLinkClickRef.current)) {\n return refuse('page_actions_not_supported');\n }\n setAgentStatus((prev) => (prev ? { ...prev, status: 'acting_on_page' } : { status: 'acting_on_page' }));\n try {\n const result = await performPageAction(action, pageRefsRef.current, onRelativeLinkClickRef.current, document, signal);\n if (signal?.aborted) return refuse('panel_closed');\n if (action.type !== 'snapshot') await waitForDomSettle(document, { signal });\n if (signal?.aborted) return refuse('panel_closed');\n // The snapshot can throw on its own (a very deep DOM, a host\n // `getComputedStyle` that raises). It rides in `error` while `ok` stays\n // the action's own result: `ok: false` would have the agent retry a click\n // that already ran, and a field of its own would be dropped unseen by a\n // backend that does not know it, leaving an empty page and no sign why.\n let page = { text: '', url: '', title: '' };\n let snapshotError: string | null = null;\n try {\n const snapshot = takePageSnapshot(document);\n pageRefsRef.current = snapshot.refs;\n page = { text: snapshot.text, url: snapshot.url, title: snapshot.title };\n } catch (err) {\n pageRefsRef.current = new Map();\n snapshotError = `${err instanceof Error ? err.message : String(err)}`.slice(0, 500);\n }\n await fetch(approveUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeadersRef.current ?? {}) },\n signal,\n body: JSON.stringify({\n conversation_id: convId,\n page_action: {\n action_id: actionId,\n ok: result.ok,\n error: result.error ?? (snapshotError === null ? null : `snapshot_failed: ${snapshotError}`),\n url: page.url,\n title: page.title,\n snapshot: page.text,\n },\n }),\n });\n } catch {\n // The turn's own page wait ends it; there is nothing to show here.\n } finally {\n // Not for a turn that was stopped: its replacement owns the status line.\n if (!signal?.aborted) {\n setAgentStatus((prev) => (prev?.status === 'acting_on_page' ? { ...prev, status: 'thinking' } : prev));\n }\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 let accumulated = '';\n let doneReceived = false;\n\n const deltas = createStreamDeltas((textChanged, thinking) => {\n if (textChanged) {\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 }\n if (thinking) {\n setAgentStatus((prev) => ({\n ...prev,\n status: prev?.status ?? 'thinking',\n thinkingContent: (prev?.thinkingContent ?? '') + thinking,\n }));\n }\n });\n streamDeltasRef.current = deltas;\n\n const controller = new AbortController();\n pageActionControllerRef.current?.abort();\n const pageActions = new AbortController();\n try {\n abortControllerRef.current = controller;\n pageActionControllerRef.current = pageActions;\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 supportsPageActions: hostTakesPageActions(getApproveUrl(), onRelativeLinkClickRef.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\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 // Nothing of the closed segment may land in the new one.\n deltas.flush();\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 === 'thinking_text') {\n deltas.thinking(parsed.thinkingContent ?? '');\n break;\n }\n // Every other status lands after the text and reasoning that\n // arrived before it.\n deltas.flush();\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 === '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 deltas.text();\n break;\n }\n\n case 'done': {\n deltas.flush();\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 // The backend stores a segment when its `done` is sent, and a\n // restore shows that time: stamping the send time instead would\n // change the footer on the next reload.\n const completedAt = new Date();\n setMessages((prev) =>\n prev.map((m) =>\n m.id === segId\n ? {\n ...m,\n content: finalContent,\n timestamp: completedAt,\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 serverId: parsed.messageId ?? m.serverId,\n }\n : m,\n ),\n );\n break;\n }\n\n case 'page_action_required': {\n deltas.flush();\n // Not awaited: the stream stays silent until the answer lands,\n // and the reader must keep draining its keep-alives meanwhile.\n void answerPageAction(parsed.actionId, parsed.pageAction, parsed.conversationId);\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 deltas.flush();\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 deltas.flush();\n const segId = currentAssistantId;\n setMessages((prev) =>\n prev.map((m) =>\n // The backend reported a failure of its own, so the generic\n // apology rather than \"check the configuration\": the\n // connection plainly worked.\n m.id === segId ? { ...m, content: parsed.content || t('Sorry, an error occurred. Please try again.') } : 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 deltas.flush();\n if (accumulated && !doneReceived) {\n const segId = currentAssistantId;\n const text = accumulated;\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: text || t('No response.') } : m)));\n }\n } catch (err) {\n // A frame still pending would land after the error below, or in the\n // chat that replaced an aborted stream.\n deltas.close();\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 deltas.close();\n if (streamDeltasRef.current === deltas) streamDeltasRef.current = null;\n // The turn is over, so its page actions are too - but only this turn's:\n // a replacement already owns the refs by then.\n if (pageActionControllerRef.current === pageActions) pageActions.abort();\n if (abortControllerRef.current === controller) 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 // Nothing of the abandoned stream may land in the fresh chat.\n streamDeltasRef.current?.close();\n streamDeltasRef.current = null;\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n pageActionControllerRef.current?.abort();\n pageActionControllerRef.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 // The text that arrived before the stop stays on screen, including what the\n // next frame would have applied; nothing after it does.\n streamDeltasRef.current?.flush();\n streamDeltasRef.current?.close();\n streamDeltasRef.current = null;\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n pageActionControllerRef.current?.abort();\n pageActionControllerRef.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 pageRefsRef.current = new Map();\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 // A panel closed mid-stream has no state left for the stream's frames to\n // update. Its page actions stop, so a hover fallback cannot click the host\n // page after the panel closed; the stream itself runs on, which is how its\n // answer is in history when the panel reopens.\n useEffect(\n () => () => {\n streamDeltasRef.current?.close();\n pageActionControllerRef.current?.abort();\n },\n [],\n );\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","/**\n * Streamed answer text and reasoning, applied to React state at most once per\n * animation frame.\n *\n * A backend can emit one event per model delta, dozens a second: applied one by\n * one, each re-renders the panel and re-parses the live answer, far more often\n * than the screen can show. The deltas are gathered here and applied together\n * on the next frame. Every other event is handled after a `flush()`, so state\n * still changes in the order the events arrived.\n *\n * A hidden tab runs no frames, and needs none: what waits is the text itself,\n * not a queue of events, and the first frame once the tab is visible applies\n * it - or, before that, the next event of another kind or the end of the\n * stream.\n */\nexport interface StreamDeltas {\n /** The answer text grew; the applier reads the text itself. */\n text: () => void;\n /** A chunk of reasoning prose arrived. */\n thinking: (chunk: string) => void;\n /** Apply what is pending now. */\n flush: () => void;\n /** Drop what is pending and ignore any later delta: the stream is over or abandoned. */\n close: () => void;\n}\n\nexport function createStreamDeltas(apply: (textChanged: boolean, thinking: string) => void): StreamDeltas {\n let textChanged = false;\n let thinking = '';\n let frame: number | null = null;\n let closed = false;\n\n const cancelFrame = () => {\n if (frame !== null) cancelAnimationFrame(frame);\n frame = null;\n };\n const flush = () => {\n cancelFrame();\n if (!textChanged && !thinking) return;\n const changed = textChanged;\n const chunk = thinking;\n textChanged = false;\n thinking = '';\n apply(changed, chunk);\n };\n const schedule = () => {\n if (frame !== null) return;\n if (typeof requestAnimationFrame !== 'function') {\n flush();\n return;\n }\n frame = requestAnimationFrame(() => {\n frame = null;\n flush();\n });\n };\n\n return {\n text: () => {\n if (closed) return;\n textChanged = true;\n schedule();\n },\n thinking: (chunk) => {\n if (closed) return;\n thinking += chunk;\n schedule();\n },\n flush,\n close: () => {\n closed = true;\n cancelFrame();\n textChanged = false;\n thinking = '';\n },\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';\nimport { translate } from '../utils';\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 ? translate(t, '{agent} has finished', { agent: agentName }) : 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';\nimport { parseQuota } from '../utils/quota';\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\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 type { ChatQuotaStatus, Translate } from '../types';\n\n/**\n * The quota the composer indicator shows, as the quota route sends it.\n *\n * Kept free of local runtime imports so `node --test` runs it as is.\n */\n\n/** Defensive parse: an unexpected payload shows no indicator rather than a wrong one. */\nexport function 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 ...(q.scope === 'global' || q.scope === 'user' ? { scope: q.scope } : {}),\n };\n}\n\n/**\n * The period a limit applies to, as the tooltip words it: the periods XTM One\n * sets a quota for are translated, anything else is shown as sent.\n */\nexport function quotaPeriodLabel(period: string, t: Translate): string {\n switch (period) {\n case 'daily':\n return t('today');\n case 'monthly':\n return t('this month');\n case 'yearly':\n return t('this year');\n default:\n return period;\n }\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 { useCallback, useInsertionEffect, useRef } from 'react';\n\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const keys = Object.keys(a);\n if (keys.length !== Object.keys(b).length) return false;\n return keys.every(\n (key) => Object.prototype.hasOwnProperty.call(b, key) && Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\n/**\n * `value`, keeping the previous object while its keys and values are the same.\n *\n * For plain-data props a host builds inline (headers, endpoint paths, page\n * context): a new but equal object on every host render would otherwise break\n * the memoized message rows and re-run every effect keyed on it — refetching\n * agents, prompts, quota and suggestions, and every image fetched with the\n * headers.\n */\nexport function useShallowStable<T>(value: T): T {\n const ref = useRef(value);\n if (!shallowEqual(ref.current, value)) ref.current = value;\n return ref.current;\n}\n\n/**\n * A function with a stable identity that always calls the latest `callback`,\n * or `undefined` while there is none (callers test for presence to decide what\n * to render).\n */\nexport function useLatestCallback<Args extends unknown[], Result>(\n callback: ((...args: Args) => Result) | undefined,\n): ((...args: Args) => Result) | undefined {\n const ref = useRef(callback);\n // Updated before any layout or passive effect runs, so an effect of this\n // render never calls the previous callback.\n useInsertionEffect(() => {\n ref.current = callback;\n });\n const stable = useCallback((...args: Args) => ref.current?.(...args) as Result, []);\n return callback ? stable : undefined;\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\nconst SPEAKER =\n 'M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z';\n\nexport const VolumeIcon = ({ 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={SPEAKER} />\n <path d=\"M16 9a5 5 0 0 1 0 6\" />\n <path d=\"M19.364 18.364a9 9 0 0 0 0-12.728\" />\n </svg>\n);\n\nexport const VolumeOffIcon = ({ 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={SPEAKER} />\n <path d=\"m22 9-6 6\" />\n <path d=\"m16 9 6 6\" />\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","/**\n * Where a tooltip is drawn.\n *\n * It is centred above its anchor. Near the side of the panel (the time under a\n * message) it would run past it - into the host page, or off the screen - and\n * near the top it would run above it, into the host app's top bar. So its width\n * is capped to the room there is (a long label wraps instead), it is shifted back\n * inside, and it opens below its anchor when there is no room above.\n */\n\n/** Space between a tooltip and its anchor. */\nexport const ANCHOR_GAP = 4;\n\n/** Space kept between a tooltip and the side of the panel it would cross. */\nexport const EDGE_MARGIN = 4;\n\n/** A box in viewport coordinates, as `getBoundingClientRect` returns it. */\nexport interface Box {\n readonly top: number;\n readonly bottom: number;\n readonly left: number;\n readonly right: number;\n}\n\n/** A tooltip's anchor and the room it may take: the part of the panel inside the viewport, less the margins. */\nexport interface TooltipOpening {\n readonly top: number;\n readonly bottom: number;\n readonly center: number;\n readonly minTop: number;\n readonly minLeft: number;\n readonly maxRight: number;\n}\n\nexport interface TooltipPlacement {\n /** Drawn below its anchor instead of above. */\n readonly below: boolean;\n /** Horizontal offset from the anchor's centre that keeps it inside. */\n readonly shift: number;\n}\n\nexport function tooltipOpening(anchor: Box, panel: Box, viewportWidth: number): TooltipOpening {\n return {\n top: anchor.top,\n bottom: anchor.bottom,\n center: (anchor.left + anchor.right) / 2,\n minTop: Math.max(panel.top, 0),\n minLeft: Math.max(panel.left, 0) + EDGE_MARGIN,\n maxRight: Math.min(panel.right, viewportWidth) - EDGE_MARGIN,\n };\n}\n\n/** The widest a tooltip may be drawn: wider than that, it would cross the panel's side whatever its shift. */\nexport function tooltipMaxWidth(opening: TooltipOpening): number {\n return Math.max(opening.maxRight - opening.minLeft, 0);\n}\n\n/** Places a tooltip of the measured size, drawn no wider than `tooltipMaxWidth`. */\nexport function placeTooltip(opening: TooltipOpening, size: { readonly width: number; readonly height: number }): TooltipPlacement {\n const left = opening.center - size.width / 2;\n let shift = 0;\n if (left < opening.minLeft) shift = opening.minLeft - left;\n else if (left + size.width > opening.maxRight) shift = opening.maxRight - (left + size.width);\n return { below: opening.top - ANCHOR_GAP - size.height < opening.minTop, shift };\n}\n","import { useLayoutEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { findChatbotRoot } from '../utils';\nimport { ANCHOR_GAP, placeTooltip, tooltipMaxWidth, tooltipOpening, type TooltipOpening } from '../utils/tooltipPlacement';\n\ninterface TooltipProps {\n title: string;\n children: React.ReactElement;\n}\n\nexport const Tooltip = ({ title, children }: TooltipProps) => {\n const ref = useRef<HTMLSpanElement>(null);\n const tipRef = useRef<HTMLSpanElement>(null);\n const [opening, setOpening] = useState<TooltipOpening | null>(null);\n const [placement, setPlacement] = useState({ below: false, shift: 0 });\n\n // Its size is its text's, wrapped to the room there is: measured once shown,\n // before the browser paints it. The tooltip lives inside the panel's stacking\n // context (z-[1200] in sidebar mode), so anything drawn above the panel lands\n // in the host app's top-bar zone and is hidden whenever the host bar stacks\n // higher (e.g. OpenAEV's AppBar at theme.zIndex.drawer + 1).\n useLayoutEffect(() => {\n if (!opening || !tipRef.current) return;\n const { width, height } = tipRef.current.getBoundingClientRect();\n setPlacement(placeTooltip(opening, { width, height }));\n }, [opening, title]);\n\n if (!title) return children;\n\n const open = () => {\n if (!ref.current) return;\n const panel = findChatbotRoot(ref.current).getBoundingClientRect();\n setOpening(tooltipOpening(ref.current.getBoundingClientRect(), panel, window.innerWidth));\n };\n\n // A keyboard user reaches the control without hovering it, so its tooltip\n // opens on focus too - on keyboard focus only: a click also focuses, and the\n // hover has already shown the tooltip then.\n const handleFocus = (event: React.FocusEvent) => {\n const target = event.target as HTMLElement;\n if (typeof target.matches === 'function' && target.matches(':focus-visible')) open();\n };\n\n return (\n <span\n ref={ref}\n className=\"inline-flex\"\n onMouseEnter={open}\n onMouseLeave={() => setOpening(null)}\n onFocus={handleFocus}\n onBlur={() => setOpening(null)}\n >\n {children}\n {opening &&\n createPortal(\n <span\n ref={tipRef}\n className={`pointer-events-none fixed z-[10001] w-max -translate-x-1/2 ${placement.below ? '' : '-translate-y-full'} whitespace-normal break-words 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={{\n top: placement.below ? opening.bottom + ANCHOR_GAP : opening.top - ANCHOR_GAP,\n left: opening.center + placement.shift,\n maxWidth: tooltipMaxWidth(opening),\n }}\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, Translate, XtmAgent } from '../types';\nimport { timeAgo, translate } 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: Translate;\n}\n\n// Labels are thunks rather than bare strings so each key stays a literal call\n// on the host's translator in the source: a table of plain keys resolved later\n// through a variable works at runtime but is invisible to key extraction, which\n// is how a locale ends up missing exactly the strings nobody typed by hand.\nconst modeOptions: { mode: ChatMode; label: (t: Translate) => string; getIcon: (p: { size: number; className: string }) => React.ReactNode }[] = [\n { mode: 'floating', label: (t) => t('Floating'), getIcon: (p) => <FloatingIcon {...p} /> },\n { mode: 'sidebar', label: (t) => t('Sidebar'), getIcon: (p) => <SidebarIcon {...p} /> },\n { mode: 'fullscreen', label: (t) => t('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 {translate(t, 'Transferred from {agent}', { agent: 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\">{opt.label(t)}</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","/**\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}\nexport interface SpeechRecognitionEventLike {\n resultIndex: number;\n results: { length: number; [index: number]: SpeechRecognitionResultLike };\n}\nexport interface SpeechRecognitionLike {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n start: () => void;\n stop: () => void;\n abort: () => void;\n onresult: ((event: SpeechRecognitionEventLike) => void) | null;\n onerror: (() => void) | null;\n onend: (() => void) | null;\n}\n\nexport interface DictationEvents {\n /** A finalised phrase, trimmed, to append to the draft. */\n onFinal: (text: string) => void;\n /** Words heard but not final yet; an empty string clears the preview. */\n onInterim: (text: string) => void;\n onListeningChange: (listening: boolean) => void;\n}\n\nexport interface DictationController {\n /** Start listening, or stop and commit the phrase already heard. */\n toggle: () => void;\n /** Stop listening and drop whatever the session still delivers. */\n cancel: () => void;\n /** Release the recogniser; no event is reported afterwards. */\n dispose: () => void;\n}\n\n/**\n * - `listening`: a session the user wants; the engine ending it after a pause\n * starts it again.\n * - `stopping`: the user stopped it; the phrase in flight is still committed.\n * - `discarding`: cancelled; everything it delivers until its `end` is dropped.\n */\ntype Phase = 'idle' | 'listening' | 'stopping' | 'discarding';\n\n/**\n * Drives one recogniser through successive dictation sessions.\n *\n * A session keeps emitting after `stop()` / `abort()` until its `end` event,\n * and no event says which session it belongs to. The recogniser also refuses\n * `start()` until that `end`, so every event is read against the phase of the\n * one session it can belong to, and a start asked for while the previous\n * session is still ending waits for that `end` instead of failing.\n */\nexport function createDictationController(recognition: SpeechRecognitionLike, events: DictationEvents): DictationController {\n let phase: Phase = 'idle';\n let startQueued = false;\n let disposed = false;\n let reported = false;\n\n const isOn = () => phase === 'listening' || startQueued;\n\n const report = () => {\n if (disposed || isOn() === reported) return;\n reported = isOn();\n events.onListeningChange(reported);\n };\n\n const tryStart = () => {\n try {\n recognition.start();\n return true;\n } catch {\n return false;\n }\n };\n\n const start = () => {\n if (tryStart()) phase = 'listening';\n else if (phase !== 'idle') startQueued = true;\n report();\n };\n\n const stop = () => {\n startQueued = false;\n if (phase === 'listening') {\n phase = 'stopping';\n events.onInterim('');\n try {\n recognition.stop();\n } catch {\n phase = 'idle';\n }\n }\n report();\n };\n\n const cancel = () => {\n if (disposed) return;\n startQueued = false;\n if (phase === 'listening' || phase === 'stopping') {\n phase = 'discarding';\n events.onInterim('');\n try {\n recognition.abort();\n } catch {\n phase = 'idle';\n }\n }\n report();\n };\n\n recognition.onresult = (event) => {\n if (phase !== 'listening' && phase !== 'stopping') return;\n let finalText = '';\n let interimText = '';\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 const phrase = finalText.trim();\n if (phrase) events.onFinal(phrase);\n if (phase === 'listening') events.onInterim(interimText.trim());\n };\n\n recognition.onerror = () => {\n // Permission denied, no microphone, network failure: let the session end\n // rather than restart it into the same failure.\n if (phase === 'listening') {\n phase = 'stopping';\n events.onInterim('');\n }\n report();\n };\n\n recognition.onend = () => {\n events.onInterim('');\n if (phase === 'listening') {\n if (tryStart()) return;\n phase = 'idle';\n } else if (startQueued) {\n startQueued = false;\n phase = tryStart() ? 'listening' : 'idle';\n } else {\n phase = 'idle';\n }\n report();\n };\n\n return {\n toggle: () => {\n if (disposed) return;\n if (isOn()) stop();\n else start();\n },\n cancel,\n dispose: () => {\n if (disposed) return;\n disposed = true;\n recognition.onresult = null;\n recognition.onerror = null;\n recognition.onend = null;\n if (phase !== 'idle') {\n try {\n recognition.abort();\n } catch {\n /* already ended */\n }\n }\n phase = 'idle';\n startQueued = false;\n },\n };\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { createDictationController, type DictationController, type SpeechRecognitionLike } from './dictationController';\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 /** Start listening, or stop and still commit the phrase already heard. */\n toggle: () => void;\n /** Stop listening and drop anything still on its way, e.g. once the draft is sent. */\n cancel: () => 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 controllerRef = useRef<DictationController | null>(null);\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 const controller = createDictationController(recognition, {\n onFinal: (text) => onFinalTextRef.current(text),\n onInterim: setInterim,\n onListeningChange: setListening,\n });\n controllerRef.current = controller;\n return () => {\n controllerRef.current = null;\n controller.dispose();\n setListening(false);\n setInterim('');\n };\n }, []);\n\n const toggle = useCallback(() => controllerRef.current?.toggle(), []);\n const cancel = useCallback(() => controllerRef.current?.cancel(), []);\n\n return { supported, listening, interim, toggle, cancel };\n}\n","import { useRef, useState } from 'react';\nimport type { ChatContextBreakdown, ChatContextUsage, Translate } from '../types';\nimport { compactCount, translate } from '../utils';\nimport { Dropdown } from './Dropdown';\nimport { Tooltip } from './Tooltip';\n\ninterface ContextUsageIndicatorProps {\n usage: ChatContextUsage;\n t: Translate;\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: (t: Translate) => string; color: string }> = [\n { field: 'system', label: (t) => t('System prompt'), color: '#9ca3af' },\n { field: 'tools', label: (t) => t('Tool definitions'), color: '#a78bfa' },\n { field: 'dynamicTools', label: (t) => t('MCP & dynamic tools'), color: '#f0abfc' },\n { field: 'summary', label: (t) => t('Summarized conversation'), color: '#fb7185' },\n { field: 'toolResults', label: (t) => t('Tool results'), color: '#34d399' },\n { field: 'conversation', label: (t) => t('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 tokens = translate(t, '{counts} tokens', { counts });\n // Separator only — nothing to translate, so no key of its own: both halves\n // are already whole translated readouts.\n const summary = `${headline} · ${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 cx={SIZE / 2} cy={SIZE / 2} r={RADIUS} fill=\"none\" strokeWidth={STROKE} className=\"stroke-gray-200 dark:stroke-white/15\" />\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 ? '' : translate(t, '{summary} — click for details', { summary })}>\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}`}>{translate(t, '{percent}% full', { 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\">~{tokens}</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\">{row.label(t)}</span>\n <span className=\"ml-auto text-[0.7rem] tabular-nums text-gray-500 dark:text-white/40 shrink-0\">\n {compactCount(breakdown?.[row.field] ?? 0)}\n </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, translate } from '../utils';\nimport { quotaPeriodLabel } from '../utils/quota';\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 } = quota;\n const period = quotaPeriodLabel(quota.period, t);\n // One allowance for the whole platform: say so, or a user reads colleagues'\n // consumption as their own.\n const withScope = (title: string) => (quota.scope === 'global' ? translate(t, '{quota} (shared across all users)', { quota: title }) : title);\n\n // No ceiling: report consumption without implying a limit that isn't there.\n if (limit === null) {\n const title = withScope(period ? translate(t, 'Usage · {period}', { period }) : t('Usage'));\n return (\n <Tooltip title={title}>\n <span className=\"text-[0.68rem] tabular-nums text-gray-400 dark:text-white/30\" role=\"img\" aria-label={`${title} ${compact(used)}`}>\n {compact(used)}\n </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 headline = exhausted ? t('Quota reached') : t('Quota');\n const title = withScope(period ? `${headline} · ${period}` : headline);\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 { useLayoutEffect, 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\nconst MAX_TEXTAREA_HEIGHT = 120;\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 // A dictated phrase lands at the end of the draft while the focus is on the\n // mic, so nothing scrolls the field to it once the text outgrows the cap.\n const revealEndRef = useRef(false);\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 revealEndRef.current = true;\n onInputChange(inputValue.trim() ? `${inputValue.trimEnd()} ${finalText}` : finalText);\n });\n\n // Sized from the value rather than from the textarea's own change event:\n // dictation, template picks, draft restores and the clear after a send all\n // change the text without one.\n useLayoutEffect(() => {\n const el = textareaRef.current;\n if (!el) return;\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, MAX_TEXTAREA_HEIGHT)}px`;\n if (revealEndRef.current) {\n revealEndRef.current = false;\n el.scrollTop = el.scrollHeight;\n }\n }, [inputValue]);\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 // The toolbar row costs vertical space, so it only exists when something\n // actually occupies it. Dictation is not among them: it acts on the text\n // field, so it lives inside it, next to Send.\n const hasToolbar = Boolean((prompts && prompts.length > 0) || quota || contextUsage || composerToolbar);\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 // Every way of sending goes through here, and only when the matching button\n // would be enabled: Enter must not send while files upload, nor end a\n // dictation that nothing was sent from. Dictation is cancelled, not stopped:\n // a phrase still being recognised would land in the composer just emptied.\n const send = () => {\n if (!(isLoading ? showSteerSend : canSend)) return;\n dictation.cancel();\n onSend();\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n if (e.key === 'Escape' && isLoading) {\n e.preventDefault();\n onStop();\n }\n };\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 {/* items-end keeps the buttons on the last line once the field grows. */}\n <div\n className={`flex items-end border rounded-xl px-2 py-1 transition-colors ${\n dictation.listening ? 'border-red-500/50' : 'border-gray-200 dark:border-white/10 focus-within:border-[var(--chat-accent)]'\n }`}\n >\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={dictation.listening ? t('Listening...') : t('Ask a question...')}\n value={inputValue}\n onChange={(e) => onInputChange(e.target.value)}\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: MAX_TEXTAREA_HEIGHT }}\n />\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={t('Dictate a message')}\n aria-pressed={dictation.listening}\n className={`relative w-8 h-8 flex items-center justify-center shrink-0 rounded-lg mr-0.5 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={18} /> : <MicIcon size={18} />}\n {dictation.listening && <span className=\"absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-red-500 motion-safe:animate-pulse\" />}\n </button>\n </Tooltip>\n )}\n {showSteerSend && (\n <Tooltip title={t('Send now')}>\n <button\n type=\"button\"\n onClick={send}\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 : send}\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 {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 {/* Words heard but not yet final take the footer's place, so the preview\n never shifts the composer's height. */}\n {dictation.interim ? (\n <p className=\"flex items-center justify-center gap-1.5 text-[0.65rem] italic text-red-500 dark:text-red-400 mt-1.5\">\n <span className=\"w-1.5 h-1.5 shrink-0 rounded-full bg-red-500 motion-safe:animate-pulse\" />\n <span className=\"truncate\">{dictation.interim}</span>\n </p>\n ) : (\n <p className=\"text-center text-[0.65rem] text-gray-400 dark:text-white/30 mt-1.5 opacity-70\">{footerText}</p>\n )}\n </div>\n );\n};\n","import { useCallback, useInsertionEffect, useRef, useState } from 'react';\nimport type { ChatMessage, MessageFeedback, Translate } from '../types';\nimport { normalizeFeedbackComment, ratingOf, submitFeedback } from '../utils/feedback';\n\n/** What the user did to one answer's rating in this session. */\nexport interface FeedbackEntry {\n /** Identifies the last change: a request answered after a newer one is ignored. */\n seq: number;\n value: MessageFeedback | null;\n comment: string | null;\n /** A request is on its way; the thumbs wait for it. */\n pending: boolean;\n /** The translated reason the last request failed, shown under the answer. */\n error: string | null;\n /** The optional comment asked for after a thumbs down. */\n commentOpen: boolean;\n}\n\n/**\n * The key an answer's rating is kept under. Not `id` alone: a restore renames\n * every row (`restored-<n>`), and one runs right after the first turn of a new\n * conversation - the persisted id is what stays the same.\n */\nexport const feedbackKeyOf = (msg: ChatMessage): string => msg.serverId ?? msg.id;\n\ninterface Options {\n /** `{apiBaseUrl}{feedback}` when the panel stores ratings itself (`feedbackBaseUrl`). */\n feedbackUrl?: string | null;\n conversationId?: string | null;\n requestHeaders?: Record<string, string>;\n onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;\n t: Translate;\n}\n\n/**\n * The thumbs of every answer in the transcript. A rating shows at once and is\n * taken back if the backend refuses it; the callbacks keep their identity, so\n * the memoized rows do not re-render when the panel does.\n *\n * Every action is handed the answer's current entry by its row: a click is a\n * discrete event, so the row has rendered the previous one's outcome by then.\n */\nexport function useMessageFeedback({ feedbackUrl, conversationId, requestHeaders, onMessageFeedback, t }: Options) {\n const [state, setState] = useState<{ scope: string | null | undefined; entries: Record<string, FeedbackEntry> }>(() => ({\n scope: conversationId,\n entries: {},\n }));\n // Ratings belong to one conversation: another one starts from none, and a\n // request of the previous one answering late finds no entry to update.\n let entries = state.entries;\n if (state.scope !== conversationId) {\n entries = {};\n setState({ scope: conversationId, entries });\n }\n\n const latest = useRef({ feedbackUrl, conversationId, requestHeaders, onMessageFeedback, t });\n useInsertionEffect(() => {\n latest.current = { feedbackUrl, conversationId, requestHeaders, onMessageFeedback, t };\n });\n const seqRef = useRef(0);\n\n const put = useCallback((key: string, entry: FeedbackEntry) => setState((s) => ({ ...s, entries: { ...s.entries, [key]: entry } })), []);\n const patch = useCallback(\n (key: string, seq: number, change: (entry: FeedbackEntry) => FeedbackEntry) =>\n setState((s) => {\n const entry = s.entries[key];\n if (!entry || entry.seq !== seq) return s;\n return { ...s, entries: { ...s.entries, [key]: change(entry) } };\n }),\n [],\n );\n\n const rate = useCallback(\n async (msg: ChatMessage, next: MessageFeedback | null, current: FeedbackEntry | undefined) => {\n const { feedbackUrl: url, conversationId: convId, requestHeaders: headers, onMessageFeedback: notify, t: tr } = latest.current;\n const key = feedbackKeyOf(msg);\n const seq = ++seqRef.current;\n if (!url) {\n // The host stores it: nothing to wait for.\n put(key, { seq, value: next, comment: null, pending: false, error: null, commentOpen: false });\n notify?.(key, next, msg);\n return;\n }\n if (!convId || !msg.serverId) return;\n put(key, { seq, value: next, comment: null, pending: true, error: null, commentOpen: false });\n const stored = await submitFeedback(fetch, url, convId, msg.serverId, next ? { rating: ratingOf(next), comment: null } : null, headers);\n if (!stored) {\n const error = tr(next ? 'Could not submit feedback.' : 'Could not retract feedback.');\n patch(key, seq, () =>\n current\n ? { ...current, seq, pending: false, commentOpen: false, error }\n : { seq, value: null, comment: null, pending: false, error, commentOpen: false },\n );\n return;\n }\n // A thumbs down is worth a word on what went wrong - asked once it is\n // stored, so skipping the question loses nothing.\n patch(key, seq, (entry) => ({ ...entry, pending: false, commentOpen: next === 'down' }));\n latest.current.onMessageFeedback?.(key, next, msg);\n },\n [put, patch],\n );\n\n const saveComment = useCallback(\n async (msg: ChatMessage, text: string, current: FeedbackEntry) => {\n const { feedbackUrl: url, conversationId: convId, requestHeaders: headers, t: tr } = latest.current;\n const key = feedbackKeyOf(msg);\n const comment = normalizeFeedbackComment(text);\n if (!url || !convId || !msg.serverId || !comment) {\n put(key, { ...current, commentOpen: false, error: null });\n return;\n }\n const seq = ++seqRef.current;\n put(key, { ...current, seq, pending: true, error: null });\n const stored = await submitFeedback(fetch, url, convId, msg.serverId, { rating: 'negative', comment }, headers);\n patch(key, seq, (entry) =>\n stored\n ? { ...entry, comment, pending: false, commentOpen: false }\n : // The question stays open with what was typed, to try again.\n { ...entry, pending: false, error: tr('Could not submit feedback.') },\n );\n },\n [put, patch],\n );\n\n const skipComment = useCallback(\n (msg: ChatMessage, current: FeedbackEntry) => put(feedbackKeyOf(msg), { ...current, commentOpen: false, error: null }),\n [put],\n );\n\n return { entries, rate, saveComment, skipComment };\n}\n","import remarkGfm from 'remark-gfm';\nimport remarkParse from 'remark-parse';\nimport { unified } from 'unified';\n\n/**\n * Reading an answer aloud through the browser's speech synthesis.\n *\n * What is spoken is the text a reader sees, never the markdown that draws it:\n * no `**`, no `#`, no table pipes, no code block, no image, and no bare URL\n * spelled out character by character. The text is parsed with the parser the\n * panel renders with (remark-parse + remark-gfm), so what counts as a heading,\n * a list or a link is what the screen shows. It reads the documents the\n * transcript renders (`answerMarkdownSources`), never the raw answer: the\n * renderer splits it at its file markers and repairs it first (`prepareMarkdown`).\n *\n * Kept free of local imports so `node --test` runs it as is.\n */\n\nconst parser = unified().use(remarkParse).use(remarkGfm).freeze();\n\ntype MdNode = { type: string; value?: string; url?: string; children?: MdNode[] };\n\n/** Blocks and inline nodes a listener has nothing to hear in. */\nconst SILENT = new Set(['code', 'html', 'image', 'imageReference', 'definition', 'footnoteDefinition', 'footnoteReference', 'thematicBreak']);\n\nfunction inlineText(node: MdNode): string {\n if (SILENT.has(node.type)) return '';\n if (node.type === 'text' || node.type === 'inlineCode') return node.value ?? '';\n if (node.type === 'break') return ' ';\n const text = (node.children ?? []).map(inlineText).join('');\n // A link written as its own address (a bare URL, an autolink) is noise when\n // read out; a link with words says those words.\n if (node.type === 'link' && node.url && (text === node.url || `mailto:${text}` === node.url)) return '';\n return text;\n}\n\nfunction collectLines(node: MdNode, lines: string[]): void {\n if (SILENT.has(node.type)) return;\n switch (node.type) {\n case 'paragraph':\n case 'heading':\n lines.push(inlineText(node));\n return;\n case 'tableRow':\n lines.push(\n (node.children ?? [])\n .map(inlineText)\n .map((cell) => cell.trim())\n .filter(Boolean)\n .join(', '),\n );\n return;\n default:\n for (const child of node.children ?? []) collectLines(child, lines);\n }\n}\n\n/**\n * The words of an answer's markdown documents, one sentence-ending line per\n * block: a heading or a list item without its own punctuation gets a full\n * stop, so the voice pauses where the eye does. Each document is parsed on its own.\n */\nexport function speakableText(documents: readonly string[]): string {\n const lines: string[] = [];\n for (const markdown of documents) {\n if (markdown.trim()) collectLines(parser.parse(markdown) as MdNode, lines);\n }\n return lines\n .map((line) =>\n line\n .replace(/\\s+/g, ' ')\n .replace(/ ([.,!?;:])/g, '$1')\n .trim(),\n )\n .filter((line) => /[\\p{L}\\p{N}]/u.test(line))\n .map((line) => (/[.!?;:\\u3002\\uff01\\uff1f]$/.test(line) ? line : `${line}.`))\n .join('\\n');\n}\n\n/**\n * A fenced code block: a run of 3+ backticks or tildes, closed by a bare run of\n * the same character at least as long (`prepareMarkdown` lengthens the fence\n * around nested ones), or by the end of an answer cut short.\n */\nconst FENCED_BLOCK_RE = /^[ \\t]*((`|~)\\2{2,})[\\s\\S]*?(?:^[ \\t]*\\1\\2*[ \\t]*$|(?![\\s\\S]))/gm;\n\n/**\n * Whether an answer's markdown documents have words to read outside their code\n * blocks - what decides that it gets a \"Read aloud\" button. A cheap look rather\n * than `speakableText`: it runs for every message on screen, the full parse only on a click.\n */\nexport function hasSpeakableWords(documents: readonly string[]): boolean {\n return documents.some((markdown) => /[\\p{L}\\p{N}]/u.test(markdown.replace(FENCED_BLOCK_RE, '')));\n}\n\n/**\n * Longest utterance handed to the synthesiser. Chrome stops a single utterance\n * after roughly fifteen seconds without firing `end`, so a long answer is\n * queued as a series of short ones, cut between sentences.\n */\nexport const SPEECH_CHUNK_MAX_LENGTH = 220;\n\n/** `text` cut into utterances of at most `maxLength` characters, between sentences first, then between words. */\nexport function chunkSpeech(text: string, maxLength: number = SPEECH_CHUNK_MAX_LENGTH): string[] {\n const sentences = text\n .split(/\\n+|(?<=[.!?;:\\u3002\\uff01\\uff1f])\\s+/)\n .map((s) => s.trim())\n .filter(Boolean);\n const pieces: string[] = [];\n for (const sentence of sentences) {\n if (sentence.length <= maxLength) {\n pieces.push(sentence);\n continue;\n }\n let current = '';\n for (const word of sentence.split(' ')) {\n for (let start = 0; start < word.length; start += maxLength) {\n const part = word.slice(start, start + maxLength);\n if (!current) current = part;\n else if (current.length + 1 + part.length <= maxLength) current = `${current} ${part}`;\n else {\n pieces.push(current);\n current = part;\n }\n }\n }\n if (current) pieces.push(current);\n }\n const chunks: string[] = [];\n for (const piece of pieces) {\n const last = chunks.length - 1;\n if (last >= 0 && chunks[last].length + 1 + piece.length <= maxLength) chunks[last] = `${chunks[last]} ${piece}`;\n else chunks.push(piece);\n }\n return chunks;\n}\n\n/** The part of `window.speechSynthesis` the controller uses. */\nexport interface SpeechSynthesisLike<U> {\n speak(utterance: U): void;\n cancel(): void;\n}\n\n/** Builds one utterance; `window.SpeechSynthesisUtterance` in the browser. */\nexport type CreateUtterance<U> = (text: string, options: { lang?: string; onEnd: () => void; onError: () => void }) => U;\n\nexport interface SpeechController {\n /**\n * Reads `chunks` as the message `id`, stopping whatever was being read.\n * Returns false, and reads nothing, when there is nothing to read.\n */\n speak(id: string, chunks: string[], lang?: string): boolean;\n /** Stops reading. A no-op when nothing is being read. */\n stop(): void;\n /** The message being read, or null. */\n readonly speakingId: string | null;\n}\n\n/**\n * One reader for the whole transcript: starting a message stops the previous\n * one, and `onChange` reports which message is being read (null once it ended,\n * failed or was stopped). The synthesiser reports the end of an utterance\n * asynchronously and, on `cancel()`, fails every queued one: each reading\n * carries a generation, and only the current one's events count.\n */\nexport function createSpeechController<U>(\n synth: SpeechSynthesisLike<U>,\n createUtterance: CreateUtterance<U>,\n onChange: (id: string | null) => void,\n): SpeechController {\n let generation = 0;\n let speakingId: string | null = null;\n\n const finish = (gen: number) => {\n if (gen !== generation || speakingId === null) return;\n generation += 1;\n speakingId = null;\n onChange(null);\n };\n\n // Nothing of ours being read, nothing is cancelled: the synthesiser is the\n // page's, and the host may be using it.\n const stop = () => {\n if (speakingId === null) return;\n generation += 1;\n speakingId = null;\n synth.cancel();\n onChange(null);\n };\n\n const speak = (id: string, chunks: string[], lang?: string) => {\n stop();\n const parts = chunks.filter((c) => c.trim());\n if (!parts.length) return false;\n const gen = generation;\n speakingId = id;\n onChange(id);\n parts.forEach((part, index) => {\n const last = index === parts.length - 1;\n const utterance = createUtterance(part, {\n lang,\n onEnd: () => {\n if (last) finish(gen);\n },\n onError: () => {\n if (gen !== generation) return;\n // Ends the reading before cancelling the rest of its queue, whose\n // failures then belong to a past generation.\n finish(gen);\n synth.cancel();\n },\n });\n synth.speak(utterance);\n });\n return true;\n };\n\n return {\n speak,\n stop,\n get speakingId() {\n return speakingId;\n },\n };\n}\n","import { useCallback, useEffect, useInsertionEffect, useRef, useState } from 'react';\nimport { chunkSpeech, createSpeechController, speakableText, type SpeechController } from '../utils/speech';\n\nconst speechSupported = (): boolean =>\n typeof window !== 'undefined' && !!window.speechSynthesis && typeof window.SpeechSynthesisUtterance === 'function';\n\n/**\n * Reads answers aloud through the browser's speech synthesis, one at a time,\n * in `locale` (the browser's voice when unset). `supported` is false where the\n * browser has no synthesiser: the panel then offers no button at all.\n *\n * Whatever is being read stops when the transcript unmounts.\n */\nexport function useSpeechReader(locale?: string) {\n const [supported] = useState(speechSupported);\n const [speakingId, setSpeakingId] = useState<string | null>(null);\n const controllerRef = useRef<SpeechController | null>(null);\n const localeRef = useRef(locale);\n useInsertionEffect(() => {\n localeRef.current = locale;\n });\n\n const controller = useCallback((): SpeechController | null => {\n if (!supported) return null;\n controllerRef.current ??= createSpeechController<SpeechSynthesisUtterance>(\n window.speechSynthesis,\n (text, { lang, onEnd, onError }) => {\n const utterance = new SpeechSynthesisUtterance(text);\n if (lang) utterance.lang = lang;\n utterance.onend = onEnd;\n utterance.onerror = onError;\n return utterance;\n },\n setSpeakingId,\n );\n return controllerRef.current;\n }, [supported]);\n\n /**\n * Reads the message `id` from the markdown documents the transcript renders it\n * from (`answerMarkdownSources`), or stops it when it is the one being read.\n */\n const toggle = useCallback(\n (id: string, documents: readonly string[]) => {\n const reader = controller();\n if (!reader) return;\n if (reader.speakingId === id) reader.stop();\n else reader.speak(id, chunkSpeech(speakableText(documents)), localeRef.current);\n },\n [controller],\n );\n\n const stop = useCallback(() => controllerRef.current?.stop(), []);\n\n useEffect(() => () => controllerRef.current?.stop(), []);\n\n return { supported, speakingId, toggle, stop };\n}\n","/**\n * The time shown under a chat message.\n *\n * A message from today reads as its time alone (`14:05`); an older one leads\n * with a short date (`29 Sept, 14:05`), and one from another year carries the\n * year, so a restored conversation says when each answer was given without\n * anyone having to hover. The tooltip spells the whole date out.\n *\n * Kept free of local imports so `node --test` runs it as is.\n */\n\nexport interface MessageTime {\n /** What the footer shows. */\n label: string;\n /** The complete date and time, for the tooltip. */\n full: string;\n /** Machine-readable, for the `dateTime` attribute of a `<time>` element. */\n iso: string;\n}\n\ntype FormatKind = 'time' | 'date' | 'dateWithYear' | 'full';\n\nconst FORMAT_OPTIONS: Record<FormatKind, Intl.DateTimeFormatOptions> = {\n time: { hour: '2-digit', minute: '2-digit' },\n date: { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' },\n dateWithYear: { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' },\n full: { dateStyle: 'full', timeStyle: 'short' },\n};\n\n// Every row of a transcript formats with the same few formatters, and building\n// one costs far more than using it.\nconst formatters = new Map<string, Intl.DateTimeFormat>();\n\nfunction formatter(locale: string | undefined, kind: FormatKind): Intl.DateTimeFormat {\n const key = `${locale ?? ''}|${kind}`;\n let cached = formatters.get(key);\n if (!cached) {\n try {\n cached = new Intl.DateTimeFormat(locale || undefined, FORMAT_OPTIONS[kind]);\n } catch {\n // A malformed tag throws a RangeError: fall back to the browser's own\n // language rather than losing the time.\n cached = new Intl.DateTimeFormat(undefined, FORMAT_OPTIONS[kind]);\n }\n formatters.set(key, cached);\n }\n return cached;\n}\n\nfunction sameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\n/**\n * The footer time of a message sent at `date`, in `locale` (browser default\n * when absent), read against `now`. `null` for a date that is not one, so a\n * backend sending an unparseable `created_at` loses the time, not the row.\n */\nexport function formatMessageTime(date: Date, locale?: string, now: Date = new Date()): MessageTime | null {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;\n const kind: FormatKind = sameDay(date, now) ? 'time' : date.getFullYear() === now.getFullYear() ? 'date' : 'dateWithYear';\n return {\n label: formatter(locale, kind).format(date),\n full: formatter(locale, 'full').format(date),\n iso: date.toISOString(),\n };\n}\n","/**\n * How the thread keeps the end of the conversation in view as it grows.\n *\n * The view follows the conversation while the reader is at the bottom, stops\n * once they scroll up to read back, and follows again when they come back down\n * or send a message. While following, each streamed frame jumps to the end at\n * once: a smooth scroll restarted on every frame lags behind the text. A\n * message just sent and a conversation just opened are scrolled to smoothly,\n * and since any other scroll aborts a smooth one, the frames streamed in the\n * meantime wait for it to arrive and the view then catches up with them.\n *\n * The reader's gestures toward the top (a wheel, a swipe, a key, a press on\n * the scrollbar) stop the follow before the scroll they cause, read by the\n * predicates below. A scroll event that moved nothing changes nothing: it is\n * the event of a jump of ours, delivered a frame late, and right after such a\n * gesture it would re-arm the follow the gesture just ended. Each move is read\n * against the last position that moved more than 1 px, so a slow drift of\n * sub-pixel steps adds up to a move instead of reading as none at every step.\n */\n\n/** How near the bottom a reader scrolling down must come for the view to follow again. */\nexport const FOLLOW_THRESHOLD_PX = 100;\n\n/** How far (px) a finger must travel down the screen before the swipe counts as scrolling up. */\nexport const TOUCH_SLOP_PX = 4;\n\n/**\n * The longest a smooth scroll of ours is waited for. Browsers finish one well\n * within it; past it the view catches up anyway, since a smooth scroll cut\n * short by another one (a prompt scrolling itself into view) never reaches\n * where it was going.\n */\nconst SMOOTH_SCROLL_TIMEOUT_MS = 1000;\n\n/** The scroll container, as far as following the conversation needs it. */\nexport interface ScrollBox {\n readonly scrollTop: number;\n readonly scrollHeight: number;\n readonly clientHeight: number;\n scrollTo(options: ScrollToOptions): void;\n}\n\nexport interface ScrollFollowTimers {\n set: (callback: () => void, ms: number) => unknown;\n clear: (handle: unknown) => void;\n}\n\nexport interface ScrollFollower {\n /** Whether the view follows the conversation as it grows. */\n readonly following: boolean;\n /** Scroll smoothly to the end and follow again: a message just sent, a conversation just opened. */\n reveal: () => void;\n /** The conversation grew: keep its end in view while the view follows. */\n keepUp: () => void;\n /** The container scrolled. */\n onScroll: () => void;\n /**\n * The reader is scrolling toward the top (a wheel, a swipe, a key, the scrollbar). Stops the follow\n * at once: the scroll event only comes with the next frame, and a streamed\n * frame committed before it would pull the view back down first.\n */\n leave: () => void;\n}\n\nconst browserTimers: ScrollFollowTimers = {\n set: (callback, ms) => setTimeout(callback, ms),\n clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),\n};\n\nexport function createScrollFollower(getBox: () => ScrollBox | null, timers: ScrollFollowTimers = browserTimers): ScrollFollower {\n let following = true;\n // Where the view was last seen, which tells the reader scrolling up apart\n // from the view being moved down by a scroll of ours.\n let lastTop = 0;\n // A smooth scroll of ours still running: where it lands, and the timer that\n // stops waiting for it.\n let smooth: { target: number; timer: unknown } | null = null;\n\n // 'instant' rather than 'auto': a `scroll-behavior: smooth` stylesheet would\n // turn 'auto' smooth again.\n const jumpToEnd = (box: ScrollBox) => {\n box.scrollTo({ top: box.scrollHeight, behavior: 'instant' });\n lastTop = box.scrollTop;\n };\n\n const stopWaiting = () => {\n if (!smooth) return;\n timers.clear(smooth.timer);\n smooth = null;\n };\n\n // The smooth scroll is over: catch up with what streamed in while it ran.\n const arrived = () => {\n stopWaiting();\n const box = getBox();\n if (box && following) jumpToEnd(box);\n };\n\n return {\n get following() {\n return following;\n },\n reveal: () => {\n const box = getBox();\n if (!box) return;\n following = true;\n stopWaiting();\n const target = Math.max(0, box.scrollHeight - box.clientHeight);\n box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });\n lastTop = box.scrollTop;\n // Already at the end, or moved there at once (smooth scrolling turned off).\n if (lastTop >= target - 1) return;\n smooth = { target, timer: timers.set(arrived, SMOOTH_SCROLL_TIMEOUT_MS) };\n },\n keepUp: () => {\n const box = getBox();\n // A jump now would cut the smooth scroll short: it catches up on arrival.\n if (box && following && !smooth) jumpToEnd(box);\n },\n onScroll: () => {\n const box = getBox();\n if (!box) return;\n const top = box.scrollTop;\n const movedUp = top < lastTop - 1;\n const movedDown = top > lastTop + 1;\n if (movedUp || movedDown) lastTop = top;\n const distance = box.scrollHeight - top - box.clientHeight;\n if (movedUp) {\n // The reader scrolled up — or the browser moved the view to the new end\n // of a list that got shorter, which leaves it at the bottom.\n following = distance <= 1;\n } else if (movedDown && distance <= FOLLOW_THRESHOLD_PX) {\n following = true;\n }\n // Ours only goes down: it is over once the view reaches where it was sent\n // or the end, and a move up is the reader taking over.\n if (smooth && (movedUp || top >= smooth.target - 1 || distance <= 1)) arrived();\n },\n leave: () => {\n const box = getBox();\n // With nothing above to scroll to (a thread that fits), the reader is\n // still at the end whatever the gesture.\n if (!box || box.scrollTop <= 0) return;\n following = false;\n stopWaiting();\n },\n };\n}\n\n/** A wheel turned toward the top. A pinch (ctrl + wheel) zooms, and a mostly sideways one pans a code block or a table. */\nexport function wheelScrollsUp(event: { deltaX: number; deltaY: number; ctrlKey: boolean }): boolean {\n return !event.ctrlKey && event.deltaY < 0 && Math.abs(event.deltaY) > Math.abs(event.deltaX);\n}\n\nconst KEYS_TOWARD_TOP = new Set(['ArrowUp', 'PageUp', 'Home']);\n\n/** A key that scrolls a focused thread toward its top. */\nexport function keyScrollsUp(event: { key: string; shiftKey: boolean; altKey: boolean }): boolean {\n if (event.altKey) return false;\n return KEYS_TOWARD_TOP.has(event.key) || (event.key === ' ' && event.shiftKey);\n}\n\n/**\n * A finger moving DOWN the screen (the content follows it toward the top),\n * measured from the highest point of the touch so far, so a tap's jitter is\n * not a swipe.\n */\nexport function swipeScrollsUp(anchorY: number, y: number): boolean {\n return y - anchorY > TOUCH_SLOP_PX;\n}\n\n/**\n * Where the one finger on the screen is. `null` while two or more are down: a\n * pinch zooms and a first finger can travel down past the slop without the\n * thread scrolling at all.\n */\nexport function singleTouchY(touches: ArrayLike<{ clientY: number }>): number | null {\n return touches.length === 1 ? touches[0].clientY : null;\n}\n\n/**\n * The swipe anchor after a `touchmove` that found the finger at `y`: none\n * during a pinch, the finger's own position on its first move after one (no\n * `touchstart` marks the finger left on the screen), else the highest point.\n */\nexport function nextTouchAnchor(anchor: number | null, y: number | null): number | null {\n if (y === null) return null;\n return anchor === null ? y : Math.min(anchor, y);\n}\n\n/** A pointer press on the thread, `x` / `y` measured from its padding box. */\nexport interface ScrollPress {\n button: number;\n /** The press landed on the container itself, not on its content. */\n onContainer: boolean;\n /** The press landed on a link, a button or a field. */\n onControl: boolean;\n x: number;\n y: number;\n clientWidth: number;\n clientHeight: number;\n scrollTop: number;\n scrollHeight: number;\n}\n\n/**\n * A press that hands the view to the reader: the middle button away from a\n * control, which starts the browser's autoscroll on Windows and Linux (on a\n * link it opens a tab), or the thread's own scrollbar (`x` past its content\n * box) on the thumb or above it. The track below the thumb pages DOWN, toward\n * the end the view follows.\n */\nexport function pressTakesScrollbar(press: ScrollPress): boolean {\n if (press.button === 1) return !press.onControl;\n if (press.button !== 0 || !press.onContainer || press.x < press.clientWidth) return false;\n const thumbBottom = ((press.scrollTop + press.clientHeight) / press.scrollHeight) * press.clientHeight;\n return press.y <= thumbBottom;\n}\n\n/** What `innerBoxScrollsUp` reads of an element. */\nexport interface ScrollNode {\n readonly scrollTop: number;\n readonly scrollHeight: number;\n readonly clientHeight: number;\n}\n\nconst SCROLLING_OVERFLOW = new Set(['auto', 'scroll', 'overlay']);\n\n/**\n * Whether a gesture toward the top at `target` scrolls a box INSIDE the thread\n * instead of the thread: the browser moves the innermost box under it that can\n * still go up, and the thread only once that box is at its top. A box that\n * only clips (`overflow: hidden` — the reasoning window, pinned to its own\n * end) is not one: the gesture goes through it.\n */\nexport function innerBoxScrollsUp<T extends ScrollNode>(\n target: T | null,\n container: T,\n parentOf: (node: T) => T | null,\n overflowY: (node: T) => string,\n): boolean {\n for (let node = target; node && node !== container; node = parentOf(node)) {\n if (node.scrollTop > 0 && node.scrollHeight > node.clientHeight && SCROLLING_OVERFLOW.has(overflowY(node))) {\n return true;\n }\n }\n return false;\n}\n","import { useEffect, useId, useRef, useState } from 'react';\nimport type { ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict } from '../types';\nimport { translate } from '../utils';\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 {/* An unnamed tool is a malformed proposal, not a tool called\n \"unknown tool\": the placeholder is the package's own text and\n therefore translated here, not in the parser. */}\n <p className=\"truncate font-mono text-xs text-gray-800 dark:text-white/85\">{proposal.toolName || t('unknown tool')}</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 ? translate(t, '{decided}/{total} decided', { decided: decidedCount, total: proposals.length }) : ''}\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, useState } from 'react';\nimport type { Translate } from '../types';\nimport { ExternalLinkIcon, GamepadIcon } from './icons';\n\n/**\n * Playful, rotating \"still working on it\" messages shown below the status\n * bubble during longer waits. Kept short and upbeat so they fit on one line in\n * the narrow floating panel and read as a sense of progress rather than noise.\n */\nconst defaultMessages = (t: Translate) => [\n t('Crunching the data'),\n t('Connecting the dots'),\n t('Consulting the sources'),\n t('Thinking it through'),\n t('Reticulating splines'),\n t('Analyzing the details'),\n t('Almost there'),\n t('Putting it together'),\n t('Polishing the answer'),\n t('Wrapping things up'),\n];\n\n/** Message rotation cadence. */\nconst ROTATE_MS = 2600;\n\n/**\n * Where the waiting game is played: XTM One's arcade, which `?arcade=play`\n * opens on any XTM One page, the chat being where it lives. `xtmOneUrl` is\n * host input that ends up in an href, so only an http(s) base is linked.\n */\nexport function waitingGameUrl(xtmOneUrl: string | undefined): string | null {\n const base = xtmOneUrl?.trim().replace(/\\/+$/, '');\n if (!base || !/^https?:\\/\\//i.test(base)) return null;\n return `${base}/chat?arcade=play`;\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\ninterface ChatWaitingPanelProps {\n t: Translate;\n /** Host-level override; when false the panel is hidden entirely. */\n enabled?: boolean;\n /** Opens the arcade in place, for a host that runs it itself. Wins over `playUrl`. */\n onPlay?: () => void;\n /** Where the arcade is played, opened in a new tab. */\n playUrl?: string | null;\n /** The thread's follower: keeps its end in view while the reader is there. */\n keepEndInView: () => void;\n}\n\nconst PLAY_CLASS =\n 'inline-flex items-center gap-1.5 self-start rounded text-xs text-[var(--chat-accent)] opacity-80 transition-opacity hover:opacity-100 focus-visible:opacity-100';\n\n/**\n * Waiting experience shown below the status bubble during longer waits:\n * rotating messages, and an invitation to play XTM One's Space Invaders arcade\n * while the agent works. The package ships no game of its own; without a way\n * to reach the arcade the messages stand alone.\n */\nexport const ChatWaitingPanel = ({ t, enabled = true, onPlay, playUrl, keepEndInView }: ChatWaitingPanelProps) => {\n const messages = useMemo(() => defaultMessages(t), [t]);\n const reducedMotion = usePrefersReducedMotion();\n const [msgIndex, setMsgIndex] = useState(0);\n\n // The panel mounts below the last message but, unlike streamed reasoning or\n // answer text (which auto-scrolls on length change), nothing else triggers a\n // scroll - so at the bottom it lands under the fold. It is revealed through\n // the thread's follower, which only moves a reader still following the end:\n // one who scrolled up stays where they went, however near the end.\n useEffect(() => {\n if (enabled) keepEndInView();\n }, [enabled, keepEndInView]);\n\n // A disabled host arms no timer; `enabled` is a dep so toggling it cleans up.\n useEffect(() => {\n if (!enabled) return;\n const id = window.setInterval(() => setMsgIndex((i) => (i + 1) % messages.length), ROTATE_MS);\n return () => window.clearInterval(id);\n }, [enabled, messages.length]);\n\n if (!enabled) return null;\n\n const current = messages[msgIndex % messages.length];\n\n return (\n <div className=\"ml-11 mt-2.5 max-w-[78%]\">\n {/* Keep the live region scoped to the announced text only: wrapping the\n play link too would re-announce it alongside each message change. */}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {current}\n </span>\n {/* Every non-essential animation (fade-ins, the blinking caret) stops\n under prefers-reduced-motion; only the dimmed text rotates. */}\n <div\n className=\"flex flex-col gap-1.5 rounded-md bg-[var(--chat-accent)]/[0.03] px-3 py-2.5\"\n style={reducedMotion ? undefined : { animation: 'chat-fade-in 0.5s ease-out' }}\n >\n <span\n key={current}\n aria-hidden\n className=\"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 {onPlay ? (\n <button type=\"button\" onClick={onPlay} className={PLAY_CLASS}>\n <span aria-hidden className=\"inline-flex\">\n <GamepadIcon size={13} />\n </span>\n {t('Play Space Invaders')}\n </button>\n ) : playUrl ? (\n <a href={playUrl} target=\"_blank\" rel=\"noopener noreferrer\" className={PLAY_CLASS}>\n <span aria-hidden className=\"inline-flex\">\n <GamepadIcon size={13} />\n </span>\n {t('Play Space Invaders in XTM One')}\n <span aria-hidden className=\"inline-flex\">\n <ExternalLinkIcon size={11} />\n </span>\n </a>\n ) : null}\n </div>\n </div>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport type { AgentStatusState, IconProps, Translate } 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 { translate } from '../utils';\nimport { ChatWaitingPanel } from './ChatWaitingPanel';\n\ninterface ChatThinkingProps {\n agentStatus: AgentStatusState | null;\n logoIcon?: React.ReactNode;\n t: Translate;\n /** Host-level override for the waiting panel (messages and the invitation to play). */\n miniGameEnabled?: boolean;\n /** Opens the XTM One arcade in place; wins over `waitingGameUrl`. */\n onPlayWaitingGame?: () => void;\n /** Where the XTM One arcade is played, opened in a new tab. */\n waitingGameUrl?: string | null;\n /** The thread's follower: keeps its end in view while the reader is there. */\n keepEndInView: () => void;\n}\n\ntype IconComponent = (props: IconProps) => React.JSX.Element;\n\ninterface StatusVisual {\n label: string;\n StatusIcon: IconComponent;\n showDots: boolean;\n}\n\n/**\n * Whole-sentence keys, one per plural branch: a lookup-only `t` cannot select a\n * plural form, so the branch is chosen here and the locale translates the\n * finished sentence — including where the count sits in it.\n */\nfunction delegatingLabel(count: number, t: Translate): string {\n return count > 1 ? translate(t, 'Delegating {count} tasks…', { count }) : t('Delegating one task…');\n}\n\nfunction pollingLabel(count: number, t: Translate): string {\n return count > 1 ? translate(t, 'Waiting for {count} background tasks…', { count }) : t('Waiting for one background task…');\n}\n\nfunction collectingLabel(count: number, t: Translate): string {\n return count > 1 ? translate(t, 'Collecting results from {count} tasks…', { count }) : t('Collecting results from one task…');\n}\n\nfunction resolveStatusVisual(agentStatus: AgentStatusState | null, t: Translate): 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 return { label: delegatingLabel(count, t), 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 return { label: pollingLabel(count, t), 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 return { label: collectingLabel(count, t), 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]}…` : translate(t, '{tool} (+{count} more)…', { tool: unique[0], count: unique.length - 1 });\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 'acting_on_page':\n return { label: t('Acting on the page…'), StatusIcon: SparklesIcon, showDots: true };\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] ?? t('the agent');\n return { label: translate(t, 'Consulting {agent}…', { agent: consultName }), StatusIcon: UserPlusIcon, showDots: false };\n }\n case 'delegating': {\n const count = agentStatus.tools?.filter((n) => n === 'spawn_background_task').length ?? 0;\n return { label: delegatingLabel(count, t), StatusIcon: UserPlusIcon, showDots: false };\n }\n case 'polling': {\n const checkCount = agentStatus.tools?.filter((n) => n === 'check_task_status').length ?? 0;\n return { label: pollingLabel(checkCount, t), StatusIcon: SparklesIcon, showDots: false };\n }\n case 'collecting': {\n const fetchCount = agentStatus.tools?.filter((n) => n === 'get_task_result').length ?? 0;\n return { label: collectingLabel(fetchCount, t), StatusIcon: SparklesIcon, showDots: false };\n }\n case 'transferring': {\n const targetName = agentStatus.tools?.[0] ?? t('the agent');\n return { label: translate(t, 'Transferring to {agent}…', { agent: 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 * Most characters of the turn's reasoning the window renders. The window is\n * pinned to its newest line and shows about eight lines, while a long turn\n * accumulates hundreds of KB: laying all of it out again on every streamed\n * chunk only re-flows text nobody can see.\n */\nconst REASONING_WINDOW_MAX_CHARS = 6000;\n\n/** Fewer visible characters than this is no reasoning to show. */\nconst MIN_REASONING_CHARS = 3;\n\n/**\n * The text the reasoning window shows: the cleaned tail of the prose between\n * the turn's fenced blocks, opened at a paragraph break when one is near.\n * Fences pair up from the start of the whole text, as `cleanReasoningText`\n * pairs them: paired inside a cut tail instead, a block the cut split or a\n * block still streaming shifts every pair and strips the newest prose.\n */\nfunction reasoningWindowText(content: string): string {\n if (content.length <= REASONING_WINDOW_MAX_CHARS) return cleanReasoningText(content);\n // [start, end) of the prose around each fenced block; an unclosed fence\n // stays prose, as `cleanReasoningText` leaves it.\n const prose: [number, number][] = [];\n let from = 0;\n for (let open = content.indexOf('```'); open !== -1; open = content.indexOf('```', from)) {\n const close = content.indexOf('```', open + 3);\n if (close === -1) break;\n prose.push([from, open]);\n from = close + 3;\n }\n prose.push([from, content.length]);\n const parts: string[] = [];\n let budget = REASONING_WINDOW_MAX_CHARS;\n for (let i = prose.length - 1; i >= 0 && budget > 0; i--) {\n const [start, end] = prose[i];\n const cut = Math.max(start, end - budget);\n parts.push(content.slice(cut, end));\n // + 1: each dropped block leaves a space, as in `cleanReasoningText`.\n budget -= end - cut + 1;\n }\n let tail = parts.reverse().join(' ');\n if (budget <= 0) {\n const paragraph = tail.indexOf('\\n\\n');\n if (paragraph !== -1 && paragraph < tail.length / 2) tail = tail.slice(paragraph + 2);\n }\n return cleanReasoningText(tail);\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. `text` comes cleaned and tail-bounded from\n * `reasoningWindowText`; the glow lives on `.filigran-chat-reasoning` so\n * reduced motion can stop it.\n */\nfunction ThinkingTextBubble({ text }: { text: string }) {\n const ref = useRef<HTMLDivElement>(null);\n const [isOverflowing, setIsOverflowing] = useState(false);\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 }, [text]);\n\n return (\n <div className=\"filigran-chat-reasoning 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 <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\">{text}</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 * How long a working turn with no reasoning to show waits before the waiting\n * panel fills the space, so a quick reply never flashes it.\n */\nconst WAITING_GAME_DELAY_MS = 5000;\n\n/**\n * How long reasoning already on screen must go without a new chunk before the\n * waiting panel takes its place: long enough to outlast an ordinary tool call,\n * so the window only gives way on a genuinely long wait.\n */\nconst QUIET_REASONING_GAME_DELAY_MS = 10_000;\n\n/**\n * True once `active` has held for `delayMs` with the same `key`; false the\n * moment either changes, in that very render (the timer that fired carries\n * the key it saw). Arms no timer while `active` is false, so a host that\n * disables the waiting panel schedules no timeouts or re-renders for it.\n */\nfunction useSustained(active: boolean, delayMs: number, key = 0): boolean {\n const [firedKey, setFiredKey] = useState<number | null>(null);\n useEffect(() => {\n setFiredKey(null);\n if (!active) return;\n const id = window.setTimeout(() => setFiredKey(key), delayMs);\n return () => window.clearTimeout(id);\n }, [active, delayMs, key]);\n return active && firedKey === key;\n}\n\nexport const ChatThinking = ({\n agentStatus,\n logoIcon,\n t,\n miniGameEnabled = true,\n onPlayWaitingGame,\n waitingGameUrl,\n keepEndInView,\n}: 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 // Reasoning wins over the waiting panel: once the turn has reasoning to show,\n // the window stays through tool calls and short silences until the answer\n // streams. The panel fills a wait with no reasoning after 5 s, and takes over\n // from reasoning that has gone 10 s without a new chunk, until the next one.\n const reasoningText = useMemo(() => reasoningWindowText(thinkingContent ?? ''), [thinkingContent]);\n const showReasoning = reasoningText.length >= MIN_REASONING_CHARS;\n const waitedWithoutReasoning = useSustained(miniGameEnabled && !showReasoning, WAITING_GAME_DELAY_MS);\n const reasoningWentQuiet = useSustained(miniGameEnabled && showReasoning, QUIET_REASONING_GAME_DELAY_MS, thinkingContent?.length ?? 0);\n const showWaiting = showReasoning ? reasoningWentQuiet : waitedWithoutReasoning;\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 {showWaiting ? (\n <ChatWaitingPanel t={t} enabled={miniGameEnabled} onPlay={onPlayWaitingGame} playUrl={waitingGameUrl} keepEndInView={keepEndInView} />\n ) : showReasoning ? (\n <ThinkingTextBubble text={reasoningText} />\n ) : null}\n </>\n );\n};\n","import remarkGfm from 'remark-gfm';\nimport remarkParse from 'remark-parse';\nimport { unified } from 'unified';\n\n/**\n * The top-level blocks of a streamed markdown answer.\n *\n * The live answer is re-rendered every time the stream delivers text, and\n * parsing it whole costs ~14 ms at 8K characters and ~75 ms at 40K, on the\n * host application's main thread: past a few thousand characters its frames\n * go to re-parsing text that is already on screen. Rendered block by block\n * with a memoized renderer per block, only the blocks that can still change\n * are parsed again.\n *\n * The boundaries come from the parser the panel renders with (remark-parse +\n * remark-gfm; remark-breaks only rewrites line breaks inside a block), so\n * rendering the blocks one after the other produces what one parse of the\n * whole text produces. A block is final once a later top-level block starts\n * after a blank line with a complete first line: a blank line closes a\n * paragraph, and a written line after one cannot rejoin a list, a quote or an\n * indented block, whatever is appended to it. A block that starts without a\n * blank line before it stays open with its predecessor (`#` alone is a heading\n * that interrupts the paragraph above; `#x` is that paragraph's next line).\n * Link reference definitions and footnote definitions resolve across the whole\n * document, from a quote or a list item too, so a text holding one renders as\n * one block. They are read off the tree the split parses anyway rather than\n * matched by a line pattern: a pattern misses a label spanning lines, and\n * keeps whole a text that only looks like a definition (a TypeScript index\n * signature in a code block, a regex's `[^a-z]`).\n *\n * XTM One's web chat splits its streaming answer by the same rules; a fix to\n * one belongs in the other.\n */\n\nconst blockParser = unified().use(remarkParse).use(remarkGfm).freeze();\n\ntype BlockNode = ReturnType<typeof blockParser.parse>['children'][number];\n\nexport interface MarkdownBlocks {\n /** The text the blocks cover; `blocks.join('') === source`. */\n source: string;\n /** What to render: the top-level blocks, or the whole text when it holds a definition. */\n blocks: string[];\n /** Offset of each top-level block in `source`, kept when the text renders whole. */\n starts: number[];\n /** Top-level blocks `[0, settled)` are final while text is appended to `source`. */\n settled: number;\n /** The first top-level block holding a link reference or footnote definition, `-1` for none. */\n firstDefinition: number;\n}\n\n/** Whether *node* is or contains a link reference or footnote definition. */\nfunction holdsDefinition(node: BlockNode): boolean {\n switch (node.type) {\n case 'definition':\n case 'footnoteDefinition':\n return true;\n case 'blockquote':\n case 'list':\n case 'listItem':\n return node.children.some(holdsDefinition);\n default:\n return false;\n }\n}\n\nfunction lineStart(text: string, offset: number): number {\n return text.lastIndexOf('\\n', offset - 1) + 1;\n}\n\n/**\n * Whether the blocks before the one at *start* are final: it starts after a\n * blank line, and its first line is complete - while that line is still being\n * written it can turn into a continuation of the block above (`1` is a\n * paragraph, `10.` the next item of the ordered list before it).\n */\nfunction settlesBefore(text: string, start: number): boolean {\n if (start < 2 || text.indexOf('\\n', start) === -1) return false;\n const previousLine = text.slice(lineStart(text, start - 1), start - 1);\n return previousLine.trim() === '';\n}\n\n/**\n * Whether the region ends on an indented line (4 spaces or a tab). micromark\n * keeps an indented code block open across the blank lines after it and parses\n * the next line as if it interrupted it - `2. two` is text there and an ordered\n * list on its own - so the next block is rendered with it.\n */\nfunction endsIndented(text: string, start: number, end: number): boolean {\n const lines = text.slice(start, end).split('\\n');\n for (let index = lines.length - 1; index >= 0; index--) {\n if (lines[index].trim()) return /^(?: {4}|\\t)/.test(lines[index]);\n }\n return false;\n}\n\nfunction single(text: string): MarkdownBlocks {\n return { source: text, blocks: text ? [text] : [], starts: text ? [0] : [], settled: 0, firstDefinition: -1 };\n}\n\n/**\n * Split *text* into top-level markdown blocks.\n *\n * With *previous* - the split of a text that *text* extends - the settled\n * blocks are reused and only the rest is parsed again. Any other *previous* (a\n * retracted answer, a normalizer that rewrote earlier lines) is ignored.\n */\nexport function splitMarkdownBlocks(text: string, previous?: MarkdownBlocks | null): MarkdownBlocks {\n if (!text.trim()) return single(text);\n\n const reuse = previous && previous.settled > 0 && text.startsWith(previous.source) ? previous.settled : 0;\n const from = reuse ? previous!.starts[reuse] : 0;\n const tail = text.slice(from);\n\n const starts = previous && reuse ? previous.starts.slice(0, reuse) : [];\n // A definition in a settled block stays one; none there means the first one,\n // if any, is in the part parsed now.\n let firstDefinition = previous && reuse && previous.firstDefinition < reuse ? previous.firstDefinition : -1;\n starts.push(from);\n for (const node of blockParser.parse(tail).children) {\n const offset = node.position?.start.offset;\n if (offset !== undefined) {\n const start = from + lineStart(tail, offset);\n const last = starts[starts.length - 1];\n if (start > last && !endsIndented(text, last, start)) starts.push(start);\n }\n if (firstDefinition < 0 && holdsDefinition(node)) firstDefinition = starts.length - 1;\n }\n\n let settled = reuse;\n for (let index = starts.length - 1; index > reuse; index--) {\n if (settlesBefore(text, starts[index])) {\n settled = index;\n break;\n }\n }\n\n const blocks = firstDefinition < 0 ? starts.map((start, index) => text.slice(start, starts[index + 1] ?? text.length)) : [text];\n return { source: text, blocks, starts, settled, firstDefinition };\n}\n\n/**\n * {@link splitMarkdownBlocks} with the previous split kept between calls, so a\n * text that grows by a few characters per call is re-parsed from its last open\n * block only. Correctness never depends on the kept split: a text that does not\n * extend it is split from the start.\n */\nexport class MarkdownBlockSplitter {\n private last: MarkdownBlocks | null = null;\n\n split(text: string): string[] {\n if (this.last?.source !== text) this.last = splitMarkdownBlocks(text, this.last);\n return this.last.blocks;\n }\n}\n","import { createContext, memo, useCallback, useContext, useMemo, useState } from 'react';\nimport Markdown, { type Components } from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport remarkBreaks from 'remark-breaks';\nimport { CheckIcon, CopyIcon } from './icons';\nimport { ChatImage } from './ChatImage';\nimport { identity, markdownUrlTransform, prepareMarkdown } from '../utils';\nimport { MarkdownBlockSplitter } from '../utils/markdownBlocks';\nimport { isRelativeHref, toInternalHref } from '../utils/pageActions';\n\ninterface MarkdownMessageProps {\n content: string;\n /**\n * True while the message is still streaming: it is then rendered one\n * top-level block at a time, so a new chunk only re-parses the blocks it can\n * still change, and stays so once the stream ends. The markup is the one a\n * single parse of the finished message renders.\n */\n streaming?: boolean;\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\n\n/**\n * What the element renderers need from the message they render.\n *\n * react-markdown uses each renderer as an element TYPE, so a renderer must keep\n * its identity across renders: a new function is a new type, and React remounts\n * everything rendered with it. The renderers are therefore module-level and\n * read what varies per message from here — with renderers recreated on every\n * render, the streaming answer's DOM was rebuilt on every chunk, a selection in\n * it was lost, and each image in it was fetched again.\n */\ninterface MarkdownRenderContextValue {\n copiedBlock: string | null;\n onCopyCode: (code: string) => void;\n onRelativeLinkClick?: (href: string) => void;\n requestHeaders?: Record<string, string>;\n t: (key: string) => string;\n}\n\nconst MarkdownRenderContext = createContext<MarkdownRenderContextValue>({ copiedBlock: null, onCopyCode: () => undefined, t: identity });\n\nconst MarkdownCode = ({ className, children }: { className?: string; children?: React.ReactNode }) => {\n const { copiedBlock, onCopyCode, t } = useContext(MarkdownRenderContext);\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={() => onCopyCode(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 <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\nconst MarkdownLink = ({ href, children }: { href?: string; children?: React.ReactNode }) => {\n const { onRelativeLinkClick } = useContext(MarkdownRenderContext);\n const internalHref = toInternalHref(href);\n const routeInternally = internalHref !== null && !!onRelativeLinkClick;\n // A new tab unless the link was authored relative AND stays on this origin:\n // the second half keeps `/\\host` (which resolves off-site) out of this tab.\n const openInNewTab = !routeInternally && !(isRelativeHref(href) && internalHref !== null);\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\nconst MarkdownImage = ({ src, alt }: { src?: unknown; alt?: string }) => {\n const { requestHeaders, t } = useContext(MarkdownRenderContext);\n if (typeof src !== 'string' || !src) return null;\n // Keyed by its source: the image at a given place can change while the\n // message streams (a reference-style image resolves to each partial URL of\n // its definition as that line arrives), and a new source must not inherit the\n // previous one's fetched blob or its load error.\n return <ChatImage key={src} src={src} alt={alt || ''} requestHeaders={requestHeaders} t={t} />;\n};\n\nconst MARKDOWN_COMPONENTS: 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: MarkdownCode,\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: MarkdownLink,\n img: MarkdownImage,\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 }) => <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\nconst REMARK_PLUGINS = [remarkGfm, remarkBreaks];\n\n/** One parse of `source`, run again only when the text itself changes. */\nconst MarkdownBody = memo(({ source }: { source: string }) => (\n <Markdown remarkPlugins={REMARK_PLUGINS} urlTransform={markdownUrlTransform} components={MARKDOWN_COMPONENTS}>\n {source}\n </Markdown>\n));\n\nMarkdownBody.displayName = 'MarkdownBody';\n\n/**\n * A streamed message, one top-level block per parse: a settled block keeps its\n * rendering, and a new chunk re-parses only the blocks it can still change.\n * The blocks render as siblings, which is the markup one parse of the whole\n * text produces.\n */\nconst StreamedMarkdownBody = ({ source }: { source: string }) => {\n const [splitter] = useState(() => new MarkdownBlockSplitter());\n const blocks = useMemo(() => splitter.split(source), [splitter, source]);\n return (\n <>\n {blocks.map((block, index) => (\n <MarkdownBody key={index} source={block} />\n ))}\n </>\n );\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 — and only the blocks of it that can\n * still change.\n */\nexport const MarkdownMessage = memo(({ content, streaming = false, onRelativeLinkClick, requestHeaders, t = identity }: MarkdownMessageProps) => {\n const [copiedBlock, setCopiedBlock] = useState<string | null>(null);\n\n // Rendered by blocks once it has streamed: switching to one parse when the\n // stream ends would rebuild the whole message to produce the same markup,\n // fetching its images again behind their placeholders and dropping a\n // selection in it. A message first rendered finished (a restored thread, the\n // panel reopened) takes one parse.\n const [byBlocks, setByBlocks] = useState(streaming);\n if (streaming && !byBlocks) setByBlocks(true);\n\n // Preprocess once per `content`: every pass scans the whole message, so\n // memoizing keeps that work off the hot path for large messages.\n const processedContent = useMemo(() => prepareMarkdown(content), [content]);\n\n const onCopyCode = useCallback((code: string) => {\n navigator.clipboard.writeText(code);\n setCopiedBlock(code);\n setTimeout(() => setCopiedBlock(null), 2000);\n }, []);\n\n const renderContext = useMemo(\n () => ({ copiedBlock, onCopyCode, onRelativeLinkClick, requestHeaders, t }),\n [copiedBlock, onCopyCode, onRelativeLinkClick, requestHeaders, t],\n );\n\n return (\n <MarkdownRenderContext.Provider value={renderContext}>\n {byBlocks ? <StreamedMarkdownBody source={processedContent} /> : <MarkdownBody source={processedContent} />}\n </MarkdownRenderContext.Provider>\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, translate } 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 ? translate(t, '{count} iterations', { count: iterations }) : '',\n totalCalls === 1 ? t('1 tool call') : translate(t, '{count} tool calls', { count: totalCalls }),\n transfers.length === 1 ? t('1 transfer') : transfers.length > 1 ? translate(t, '{count} transfers', { count: transfers.length }) : '',\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, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { AgentStatusState, ChatAttachment, ChatMessage, MessageFeedback, ToolApprovalDecision, ToolApprovalProposal } from '../types';\nimport { feedbackKeyOf, useMessageFeedback, type FeedbackEntry } from '../hooks/useMessageFeedback';\nimport { useSpeechReader } from '../hooks/useSpeechReader';\nimport { answerMarkdownSources, splitFileMarkers, stripFileMarkers } from '../utils';\nimport { FEEDBACK_COMMENT_MAX_LENGTH, feedbackModeOf, shownFeedback, type FeedbackMode } from '../utils/feedback';\nimport { formatMessageTime } from '../utils/messageTime';\nimport {\n createScrollFollower,\n innerBoxScrollsUp,\n keyScrollsUp,\n nextTouchAnchor,\n pressTakesScrollbar,\n singleTouchY,\n swipeScrollsUp,\n wheelScrollsUp,\n} from '../utils/scrollFollow';\nimport { hasSpeakableWords } from '../utils/speech';\nimport {\n AlertTriangleIcon,\n CheckIcon,\n ChevronDownIcon,\n CopyIcon,\n DownloadIcon,\n FileIcon,\n InfoIcon,\n ThumbsDownIcon,\n ThumbsUpIcon,\n VolumeIcon,\n VolumeOffIcon,\n} from './icons';\nimport { ChatApprovalPrompt } from './ChatApprovalPrompt';\nimport { ChatImage } from './ChatImage';\nimport { ChatThinking } from './ChatThinking';\nimport { MarkdownMessage } from './MarkdownMessage';\nimport { ReasoningDetailsDialog } from './ReasoningDetailsDialog';\nimport { Tooltip } from './Tooltip';\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\nconst parentOf = (node: Element) => node.parentElement;\nconst overflowYOf = (node: Element) => getComputedStyle(node).overflowY;\n\n/** Arrows and Page Up move the caret of a field (the feedback comment), not the thread. */\nfunction isTypingTarget(target: EventTarget): boolean {\n return target instanceof HTMLElement && (target.isContentEditable || !!target.closest('input, textarea, select'));\n}\n\nconst CONTROLS = 'a[href], button, summary, label, input, textarea, select, [role=\"button\"], [role=\"link\"]';\n\n/** A link, a button or a field: a middle click opens it, Shift+Space presses it. */\nfunction isControl(target: EventTarget): boolean {\n return target instanceof Element && !!target.closest(CONTROLS);\n}\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 panel (messages and the invitation to play). */\n miniGameEnabled?: boolean;\n /** Opens the XTM One arcade in place; wins over `waitingGameUrl`. */\n onPlayWaitingGame?: () => void;\n /** Where the XTM One arcade is played, opened in a new tab. */\n waitingGameUrl?: string | null;\n /** Enables the 👍/👎 affordance on completed assistant messages (see `feedbackUrl`). */\n onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;\n /**\n * Where the panel stores ratings itself (`feedbackBaseUrl`): shows the thumbs\n * on every answer the backend identified, whether or not `onMessageFeedback`\n * is passed.\n */\n feedbackUrl?: string | null;\n /** The conversation on screen; its ratings are stored against it. */\n conversationId?: string | null;\n /** BCP 47 tag the times are formatted in and answers are read aloud in. */\n locale?: string;\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/**\n * The footer's secondary controls show while the message is hovered or holds\n * the focus, and always where nothing hovers (a touch screen). A keyboard user\n * tabbing onto one sees it through `focus-visible` / `focus-within`.\n */\nconst SHOWN_ON_HOVER = 'group-hover/msg:opacity-100 group-focus-within/msg:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100';\nconst FOOTER_BUTTON = 'p-1 rounded-lg transition-opacity outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)]';\nconst ACCENT_ON_HOVER = 'hover:text-[var(--chat-accent)] focus-visible:text-[var(--chat-accent)]';\nconst REVEALED_BUTTON = `opacity-0 ${SHOWN_ON_HOVER} ${ACCENT_ON_HOVER}`;\n/**\n * The reasoning details button stays dimly in view, so a turn that reasoned is\n * noticed, and reads like its neighbours once they show.\n */\nconst DIMMED_BUTTON = `opacity-50 ${SHOWN_ON_HOVER} ${ACCENT_ON_HOVER}`;\n/** A control in its \"on\" state (the given rating, the message being read) stays in view. */\nconst ACTIVE_BUTTON = 'opacity-100 text-[var(--chat-accent)]';\n\n/** When the message was sent; the tooltip spells the date out. */\nconst MessageTime = ({ timestamp, locale }: { timestamp: Date; locale?: string }) => {\n const time = formatMessageTime(timestamp, locale);\n if (!time) return null;\n return (\n <Tooltip title={time.full}>\n <time dateTime={time.iso} className=\"px-1 text-[0.65rem] tabular-nums text-gray-400 dark:text-white/40 select-none\">\n <span aria-hidden=\"true\">{time.label}</span>\n <span className=\"sr-only\">{time.full}</span>\n </time>\n </Tooltip>\n );\n};\n\n/** Copies a message as plain text. Revealed on message hover. */\nconst MessageCopyButton = ({ text, label, t }: { text: string; label: 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 const name = copied ? t('Copied!') : label;\n return (\n <Tooltip title={name}>\n <button\n type=\"button\"\n onClick={handleCopy}\n aria-label={name}\n className={`${FOOTER_BUTTON} ${copied ? 'opacity-100 text-green-500 dark:text-green-400' : REVEALED_BUTTON}`}\n >\n {copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}\n </button>\n </Tooltip>\n );\n};\n\n/**\n * 👍/👎 on a completed answer. Clicking the active value clears it. A rating\n * given before the conversation was reopened is final: only it is shown.\n */\nconst MessageFeedbackButtons = ({\n value,\n locked,\n pending,\n onChange,\n downRef,\n t,\n}: {\n value: MessageFeedback | null;\n locked: boolean;\n pending: boolean;\n onChange: (next: MessageFeedback | null) => void;\n downRef: React.RefObject<HTMLButtonElement | null>;\n t: (key: string) => string;\n}) => {\n const lockedId = useId();\n const inert = locked || pending;\n\n const button = (target: MessageFeedback, label: string) => {\n const active = value === target;\n if (locked && !active) return null;\n const Icon = target === 'up' ? ThumbsUpIcon : ThumbsDownIcon;\n return (\n <Tooltip title={locked ? t('Feedback already submitted') : label}>\n <button\n ref={target === 'down' ? downRef : undefined}\n type=\"button\"\n onClick={() => {\n if (!inert) onChange(active ? null : target);\n }}\n aria-label={label}\n aria-pressed={active}\n // Not `disabled`: a disabled button takes no focus and no hover, so\n // it could not say why it does not answer.\n aria-disabled={inert || undefined}\n aria-describedby={locked ? lockedId : undefined}\n className={`${FOOTER_BUTTON} ${active ? ACTIVE_BUTTON : REVEALED_BUTTON} ${inert ? 'cursor-default' : ''}`}\n >\n <Icon size={14} filled={active} />\n </button>\n </Tooltip>\n );\n };\n\n return (\n <>\n {button('up', t('Good response'))}\n {button('down', t('Bad response'))}\n {locked && (\n <span id={lockedId} className=\"sr-only\">\n {t('Feedback already submitted')}\n </span>\n )}\n </>\n );\n};\n\n/** The optional word on what went wrong, asked once a thumbs down is stored. */\nconst FeedbackComment = ({\n pending,\n onSave,\n onSkip,\n t,\n}: {\n pending: boolean;\n onSave: (text: string) => void;\n onSkip: () => void;\n t: (key: string) => string;\n}) => {\n const [text, setText] = useState('');\n const id = useId();\n const canSave = !pending && !!text.trim();\n\n return (\n <div className=\"mt-1 flex w-full max-w-[90%] flex-col gap-1.5 rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/[0.04] p-2\">\n <label htmlFor={id} className=\"text-[0.7rem] text-gray-600 dark:text-white/60\">\n {t('Tell us what went wrong (optional)')}\n </label>\n <textarea\n id={id}\n // Opened by the user's own thumbs down, to be typed into.\n autoFocus\n rows={2}\n maxLength={FEEDBACK_COMMENT_MAX_LENGTH}\n value={text}\n // Read-only rather than disabled while saving, so the focus stays here.\n readOnly={pending}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n e.stopPropagation();\n if (!pending) onSkip();\n } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n if (canSave) onSave(text);\n }\n }}\n placeholder={t('What could be improved?')}\n className=\"w-full resize-y rounded-md border border-gray-200 dark:border-white/10 bg-white dark:bg-transparent px-2 py-1 text-[0.75rem] leading-5 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-white/30 outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)]\"\n />\n <div className=\"flex justify-end gap-1.5\">\n <button\n type=\"button\"\n onClick={() => {\n if (!pending) onSkip();\n }}\n aria-disabled={pending || undefined}\n className=\"rounded-md px-2 py-0.5 text-[0.7rem] text-gray-500 dark:text-white/50 transition-colors hover:text-[var(--chat-accent)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)]\"\n >\n {t('Skip')}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n if (canSave) onSave(text);\n }}\n aria-disabled={!canSave || undefined}\n className={`rounded-md px-2 py-0.5 text-[0.7rem] text-white bg-[var(--chat-accent)] transition-opacity outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)] ${canSave ? 'hover:opacity-90' : 'opacity-50 cursor-default'}`}\n >\n {t('Save')}\n </button>\n </div>\n </div>\n );\n};\n\n/** Reads the answer aloud; the same button stops it. */\nconst ReadAloudButton = ({ speaking, onToggle, t }: { speaking: boolean; onToggle: () => void; t: (key: string) => string }) => {\n const name = speaking ? t('Stop reading') : t('Read aloud');\n return (\n <Tooltip title={name}>\n <button type=\"button\" onClick={onToggle} aria-label={name} className={`${FOOTER_BUTTON} ${speaking ? ACTIVE_BUTTON : REVEALED_BUTTON}`}>\n {speaking ? <VolumeOffIcon size={14} /> : <VolumeIcon size={14} />}\n </button>\n </Tooltip>\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 /** How this answer's thumbs work, or null for none (`feedbackModeOf`). */\n feedbackMode: FeedbackMode | null;\n /** What the user did to its rating in this session. */\n feedback: FeedbackEntry | undefined;\n onRate: (msg: ChatMessage, next: MessageFeedback | null, current: FeedbackEntry | undefined) => void;\n onSaveComment: (msg: ChatMessage, text: string, current: FeedbackEntry) => void;\n onSkipComment: (msg: ChatMessage, current: FeedbackEntry) => void;\n locale?: string;\n /** The browser can read aloud. */\n canSpeak: boolean;\n /** This message is being read aloud. */\n isSpeaking: boolean;\n onToggleSpeech: (id: string, documents: readonly string[]) => 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 feedbackMode,\n feedback,\n onRate,\n onSaveComment,\n onSkipComment,\n locale,\n canSpeak,\n isSpeaking,\n onToggleSpeech,\n t,\n }: MessageRowProps) => {\n const [showReasoning, setShowReasoning] = useState(false);\n const isAssistant = msg.role === 'assistant';\n const isEmpty = !msg.content;\n const downRef = useRef<HTMLButtonElement>(null);\n\n // The comment question takes the focus when it opens; when it closes with\n // the focus still in it (the element is gone, so the page lost it), the\n // focus goes back to the thumb that opened it rather than to the page.\n const commentOpen = !!feedback?.commentOpen;\n const wasCommentOpen = useRef(commentOpen);\n useLayoutEffect(() => {\n const closed = wasCommentOpen.current && !commentOpen;\n wasCommentOpen.current = commentOpen;\n if (closed && (!document.activeElement || document.activeElement === document.body)) downRef.current?.focus();\n }, [commentOpen]);\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\n content={part.value}\n streaming={isStreaming}\n onRelativeLinkClick={onRelativeLinkClick}\n requestHeaders={requestHeaders}\n t={t}\n />\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 const plainText = stripFileMarkers(msg.content);\n const speechDocuments = useMemo(\n () => (showActions && canSpeak ? answerMarkdownSources(msg.content) : null),\n [showActions, canSpeak, msg.content],\n );\n const shown = shownFeedback(feedback, msg.feedback);\n const speechKey = feedbackKeyOf(msg);\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 {/* The user's side mirrors the answer's: the time hugs the bubble's edge. */}\n {!isAssistant && (\n <div className=\"mt-0.5 flex items-center justify-end gap-0.5 text-gray-400 dark:text-white/40\">\n {!!msg.content.trim() && <MessageCopyButton text={msg.content} label={t('Copy message')} t={t} />}\n {!msg.timestampUnknown && <MessageTime timestamp={msg.timestamp} locale={locale} />}\n </div>\n )}\n\n {showActions && (\n <div className=\"mt-0.5 flex flex-wrap items-center gap-0.5 text-gray-400 dark:text-white/40\">\n {!msg.timestampUnknown && <MessageTime timestamp={msg.timestamp} locale={locale} />}\n {hasReasoningDetails && (\n <Tooltip title={msg.isTruncated ? t('Reasoning details — turn limit reached') : t('Reasoning details')}>\n <button\n type=\"button\"\n onClick={() => setShowReasoning((v) => !v)}\n className={`${FOOTER_BUTTON} ${\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 : DIMMED_BUTTON\n }`}\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 </Tooltip>\n )}\n <MessageCopyButton text={plainText} label={t('Copy response')} t={t} />\n {feedbackMode && (\n <MessageFeedbackButtons\n value={shown.value}\n locked={shown.locked}\n pending={!!feedback?.pending}\n onChange={(next) => onRate(msg, next, feedback)}\n downRef={downRef}\n t={t}\n />\n )}\n {speechDocuments && hasSpeakableWords(speechDocuments) && (\n <ReadAloudButton speaking={isSpeaking} onToggle={() => onToggleSpeech(speechKey, speechDocuments)} t={t} />\n )}\n </div>\n )}\n {showActions && feedback?.commentOpen && (\n <FeedbackComment\n pending={feedback.pending}\n onSave={(text) => onSaveComment(msg, text, feedback)}\n onSkip={() => onSkipComment(msg, feedback)}\n t={t}\n />\n )}\n {showActions && feedback?.error && (\n <p role=\"alert\" className=\"mt-0.5 pl-1 text-[0.7rem] text-red-600 dark:text-red-400\">\n {feedback.error}\n </p>\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 onPlayWaitingGame,\n waitingGameUrl,\n onMessageFeedback,\n isResumingAfterDecision,\n pendingApprovals,\n onSubmitApprovalDecisions,\n isSubmittingApproval,\n approvalError,\n feedbackUrl,\n conversationId,\n locale,\n t,\n}: ChatMessagesProps) => {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [renderWindow, setRenderWindow] = useState(INITIAL_RENDER_WINDOW);\n const {\n entries: feedbackEntries,\n rate,\n saveComment,\n skipComment,\n } = useMessageFeedback({\n feedbackUrl,\n conversationId,\n requestHeaders,\n onMessageFeedback,\n t,\n });\n const speech = useSpeechReader(locale);\n\n // Keeps the end of the conversation in view while the reader is at the\n // bottom (see `createScrollFollower`). It holds no React state: its timer\n // firing after unmount finds no container and does nothing.\n const [follower] = useState(() => createScrollFollower(() => scrollRef.current));\n // The highest point of the touch in progress: a swipe is measured from it,\n // so a finger that turns back down counts from where it turned.\n const touchAnchorRef = useRef<number | null>(null);\n\n // A gesture toward the top is the reader leaving the bottom — but only where\n // it moves the thread itself: an overlay portalled out of it (the image\n // lightbox) still bubbles its events here through React and scrolls nothing\n // of ours, and a box inside it that can still go up takes the gesture first.\n const leaveFrom = (container: HTMLDivElement, target: EventTarget) => {\n if (!(target instanceof Element) || !container.contains(target)) return;\n if (innerBoxScrollsUp<Element>(target, container, parentOf, overflowYOf)) return;\n follower.leave();\n };\n const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {\n if (wheelScrollsUp(event)) leaveFrom(event.currentTarget, event.target);\n };\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.defaultPrevented || !keyScrollsUp(event) || isTypingTarget(event.target)) return;\n if (event.key === ' ' && isControl(event.target)) return;\n leaveFrom(event.currentTarget, event.target);\n };\n const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n const container = event.currentTarget;\n const rect = container.getBoundingClientRect();\n const press = {\n button: event.button,\n onContainer: event.target === container,\n onControl: isControl(event.target),\n x: event.clientX - rect.left - container.clientLeft,\n y: event.clientY - rect.top - container.clientTop,\n clientWidth: container.clientWidth,\n clientHeight: container.clientHeight,\n scrollTop: container.scrollTop,\n scrollHeight: container.scrollHeight,\n };\n if (pressTakesScrollbar(press)) leaveFrom(container, event.target);\n };\n // A second finger ends the swipe: a pinch is no scroll, and the swipe of\n // the finger left on the screen is measured afresh from its next move.\n const handleTouchStart = (event: React.TouchEvent) => {\n touchAnchorRef.current = singleTouchY(event.touches);\n };\n const handleTouchMove = (event: React.TouchEvent<HTMLDivElement>) => {\n const y = singleTouchY(event.touches);\n const anchor = touchAnchorRef.current;\n if (y !== null && anchor !== null && swipeScrollsUp(anchor, y)) leaveFrom(event.currentTarget, event.target);\n touchAnchorRef.current = nextTouchAnchor(anchor, y);\n };\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 // A message the user just sent (a steer included) and a conversation just\n // opened are scrolled into view smoothly, and the view follows again.\n // Anything else — mostly the streamed answer, which changes `messages` on\n // every frame — keeps the bottom in view only while the view follows, so a\n // reader who scrolled up stays where they are.\n let userMessageCount = 0;\n for (const m of messages) if (m.role === 'user') userMessageCount += 1;\n const seenRef = useRef<{ firstMessageId?: string; userMessageCount: number } | null>(null);\n useLayoutEffect(() => {\n const seen = seenRef.current;\n seenRef.current = { firstMessageId, userMessageCount };\n if (!seen || seen.firstMessageId !== firstMessageId || userMessageCount > seen.userMessageCount) {\n follower.reveal();\n } else {\n follower.keepUp();\n }\n }, [messages, firstMessageId, userMessageCount, follower]);\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.\n const thinkingLen = agentStatus?.thinkingContent?.length ?? 0;\n useLayoutEffect(() => {\n if (thinkingLen) follower.keepUp();\n }, [thinkingLen, follower]);\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 // Reading aloud is about the message on screen: it stops when the user\n // sends a new one, opens another conversation, or the message goes away.\n const { speakingId, stop: stopSpeech } = speech;\n const spokenStillShown = speakingId === null || messages.some((m) => feedbackKeyOf(m) === speakingId);\n const speechScope = useRef({ conversationId, userMessageCount });\n useEffect(() => {\n const previous = speechScope.current;\n speechScope.current = { conversationId, userMessageCount };\n if (previous.conversationId !== conversationId || userMessageCount > previous.userMessageCount || !spokenStillShown) stopSpeech();\n }, [conversationId, userMessageCount, spokenStillShown, stopSpeech]);\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 // A tab stop of its own, so the keys scroll the thread (and are read as\n // leaving the bottom) without first focusing something inside it.\n return (\n <div\n ref={scrollRef}\n role=\"region\"\n aria-label={t('Conversation transcript')}\n tabIndex={0}\n onScroll={follower.onScroll}\n onWheel={handleWheel}\n onKeyDown={handleKeyDown}\n onPointerDown={handlePointerDown}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n className=\"flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-4 filigran-chat-scrollable outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--chat-accent-50)]\"\n >\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\n agentStatus={agentStatus}\n logoIcon={logoIcon}\n t={t}\n miniGameEnabled={miniGameEnabled}\n onPlayWaitingGame={onPlayWaitingGame}\n waitingGameUrl={waitingGameUrl}\n keepEndInView={follower.keepUp}\n />\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 feedbackMode={msg.role === 'assistant' ? feedbackModeOf(msg.serverId, feedbackUrl, conversationId, !!onMessageFeedback) : null}\n feedback={feedbackEntries[feedbackKeyOf(msg)]}\n onRate={rate}\n onSaveComment={saveComment}\n onSkipComment={skipComment}\n locale={locale}\n canSpeak={speech.supported}\n isSpeaking={speakingId === feedbackKeyOf(msg)}\n onToggleSpeech={speech.toggle}\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\n agentStatus={agentStatus}\n logoIcon={logoIcon}\n t={t}\n miniGameEnabled={miniGameEnabled}\n onPlayWaitingGame={onPlayWaitingGame}\n waitingGameUrl={waitingGameUrl}\n keepEndInView={follower.keepUp}\n />\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 {/* Holds the list's gap below its last item. */}\n <div />\n </div>\n );\n};\n","import type { Translate } from '../types';\nimport { translate, translateAround } from '../utils';\n\n/**\n * The greeting, as one sentence the locale owns end to end. The agent's name is\n * markup (it carries the accent colour), so it cannot be spliced into a string —\n * hence the split around its slot rather than three separate fragments.\n */\nconst Greeting = ({ firstName, agentName, t }: { firstName: string; agentName?: string; t: Translate }) => {\n if (!agentName) return <>{translate(t, 'How can I help you, {name}?', { name: firstName })}</>;\n const { before, after, hasSlot } = translateAround(t, 'How can {agent} help you, {name}?', 'agent', { name: firstName });\n return (\n <>\n {before}\n {hasSlot && <span className=\"text-[var(--chat-accent)]\">{agentName}</span>}\n {after}\n </>\n );\n};\n\ninterface ChatWelcomeProps {\n firstName: string;\n logoIcon: React.ReactNode;\n /** Already translated by the caller — see `ChatPanel`. */\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: Translate;\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 <Greeting firstName={firstName} agentName={agentName} t={t} />\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 {/* A host that passes no suggestions gets no section: the heading on its own\n used to sit above an empty gap. */}\n {(suggestionsLoading || promptSuggestions.length > 0) && (\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 {prompt}\n </button>\n ))\n )}\n </div>\n )}\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, ChatPanelProps, Translate } from '../types';\nimport { hexAlpha, identity } from '../utils';\nimport { feedbackBaseUrl } from '../utils/feedback';\nimport { parseContextUsage, parseRestoredMessages } 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 { useLatestCallback, useShallowStable } from '../hooks/useStableProps';\nimport { DefaultLogoIcon } from './icons';\nimport { ChatHeader } from './ChatHeader';\nimport { ChatInput } from './ChatInput';\nimport { ChatMessages } from './ChatMessages';\nimport { waitingGameUrl } from './ChatWaitingPanel';\nimport { ChatWelcome } from './ChatWelcome';\nimport { ConversationSidebar } from './ConversationSidebar';\n\nconst FLOATING_WIDTH = 380;\nconst FLOATING_HEIGHT = 560;\nconst SIDEBAR_GAP = 6;\n\n/**\n * Fallback suggestions, translated where they are written so extraction sees\n * the keys — the host's own list and the backend's are translated too (they may\n * be keys), but only these belong to the package.\n */\nconst defaultSuggestions = (t: Translate) => [\n t('Help me create a new simulation scenario'),\n t('What are the latest attack patterns?'),\n t('How do I configure detection rules?'),\n t('Summarize my recent findings'),\n];\n\nexport const ChatPanel: FunctionComponent<ChatPanelProps> = ({\n mode,\n onClose,\n onModeChange,\n topOffset = 0,\n apiBaseUrl,\n apiEndpoints: apiEndpointsProp,\n agentDashboardUrl,\n user,\n t = identity,\n accentColor = '#7b5cff',\n logoIcon,\n promptSuggestions: promptSuggestionsProp,\n draftBorderColor,\n resizable = false,\n onWidthChange,\n onResizeStart,\n onResizeEnd,\n disableFileManagement = false,\n onRelativeLinkClick: onRelativeLinkClickProp,\n onDownloadError: onDownloadErrorProp,\n maxFileCount,\n maxTotalSize,\n requestHeaders: requestHeadersProp,\n pageContext: pageContextProp,\n pushContentSelector,\n backendType = 'rest',\n miniGameEnabled = true,\n onPlayWaitingGame: onPlayWaitingGameProp,\n notifyOnComplete = true,\n onTaskComplete: onTaskCompleteProp,\n onMessageFeedback: onMessageFeedbackProp,\n locale,\n disableImagePreviews = false,\n contextUsageEnabled = true,\n composerToolbar,\n}) => {\n // Hosts tend to build these inline, so each host render hands the panel new\n // objects and functions with the same content. Held stable here, a host\n // render no longer re-renders every memoized message row, re-runs the effects\n // keyed on them (agent list, prompts, quota, suggestions, conversation list)\n // or refetches the images fetched with the headers. `t` passes through as is,\n // since a new one means new translations, and so do ReactNode props, which\n // cannot be compared.\n const apiEndpoints = useShallowStable(apiEndpointsProp);\n const requestHeaders = useShallowStable(requestHeadersProp);\n const pageContext = useShallowStable(pageContextProp);\n const promptSuggestions = useShallowStable(promptSuggestionsProp);\n const onRelativeLinkClick = useLatestCallback(onRelativeLinkClickProp);\n const onDownloadError = useLatestCallback(onDownloadErrorProp);\n const onPlayWaitingGame = useLatestCallback(onPlayWaitingGameProp);\n const onTaskComplete = useLatestCallback(onTaskCompleteProp);\n const onMessageFeedback = useLatestCallback(onMessageFeedbackProp);\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 onRelativeLinkClick: onRelativeLinkClickProp ? onRelativeLinkClick : undefined,\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 /**\n * Suggestions shown on the welcome screen, resolved to final text here rather\n * than in `ChatWelcome`: the backend's and the host's lists still go through\n * `t` (a host may well pass keys), while the package's own fallback is\n * translated at its definition so the keys stay extractable.\n */\n const suggestions = useMemo(\n () => (agentSuggestions ?? promptSuggestions)?.map((s) => t(s)) ?? defaultSuggestions(t),\n [agentSuggestions, promptSuggestions, t],\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 playUrl = useMemo(() => waitingGameUrl(agentDashboardUrl), [agentDashboardUrl]);\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 || t('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 setMessages(parseRestoredMessages(data.messages));\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, this package's list when there is neither —\n // never an empty section.\n promptSuggestions={suggestions}\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 onPlayWaitingGame={onPlayWaitingGame}\n waitingGameUrl={playUrl}\n onMessageFeedback={onMessageFeedback}\n feedbackUrl={feedbackBaseUrl(apiBaseUrl, apiEndpoints, backendType)}\n conversationId={conversationId}\n locale={locale}\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, identity } from '../utils';\nimport { DefaultLogoIcon } from './icons';\n\nexport const ChatToggleButton: FunctionComponent<ChatToggleButtonProps> = ({\n isOpen,\n onToggle,\n label,\n accentColor = '#7b5cff',\n icon,\n t = identity,\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 ?? t('Ask Assistant')}\n </button>\n );\n};\n"],"names":["hexAlpha","hex","alpha","Math","round","toString","padStart","prepareMarkdown","content","raw","lines","split","fenceRe","markupLang","openerIdx","i","length","m","match","test","trim","fences","push","idx","run","bare","blankBetween","a","b","k","depth","maxRun","nestedCount","closerIdx","j","f","next","max","fence","repeat","om","cm","join","hardenNestedCodeFences","indexOf","splitCells","row","s","startsWith","slice","endsWith","cells","current","ch","isDelimiterRow","includes","alignOf","cell","c","left","right","fenceChar","fenceLen","listMarkerRe","fenceMatch","header","delim","markerMatch","offset","trimStart","indent","headerCols","delimCells","aligns","normalizeMarkdownTables","trimmed","JSON","parse","wrapBareJson","replace","_match","alt","url","SAFE_URL_PROTOCOLS","Set","markdownUrlTransform","colon","slash","question","hash","protocol","toLowerCase","has","identity","key","translate","t","values","slot","name","Object","hasOwn","String","findChatbotRoot","el","node","classList","contains","parentElement","document","body","compactCount","n","toFixed","timeAgo","iso","then","Date","getTime","Number","isNaN","diffMs","now","minutes","floor","count","hours","days","toLocaleDateString","undefined","month","day","FILE_MARKER_RE","PARTIAL_FILE_MARKER_RE","splitFileMarkers","parts","re","lastIndex","exec","index","type","value","fileId","tail","ratingOf","feedbackModeOf","serverId","feedbackUrl","conversationId","hasCallback","normalizeFeedbackComment","comment","feedbackBaseUrl","apiBaseUrl","apiEndpoints","backendType","singleEndpoint","feedback","async","submitFeedback","fetchImpl","baseUrl","messageId","rating","requestHeaders","init","encodeURIComponent","method","credentials","headers","stringify","feedbackRequest","ok","BREAKDOWN_KEYS","parseContextUsage","evt","used","context_tokens","limit","context_window","isFinite","breakdown","src","out","any","wireKey","field","parseBreakdown","context_breakdown","parseAttachments","Array","isArray","item","file_id","filename","size","contentType","content_type","fileTag","file_tag","parseToolCallTrace","e","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","event","action_id","pageAction","requested","str","v","path","ref","text","parsePageAction","actionId","conversation_id","elapsedS","elapsed_s","contextUsage","proposals","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","message_id","parsePersistedFeedback","parseLegacyEvent","eventType","data","nodeId","activeNodeId","usedTools","map","tool","chatId","parseAgUiEvent","message","stepName","delta","toolCallName","hostTakesPageActions","approveUrl","onNavigate","baseHref","doc","defaultView","location","href","window","toInternalHref","base","from","URL","origin","pathname","search","insidePanel","closest","SECRET_AUTOCOMPLETE","hover","view","ownerDocument","bubbles","cancelable","pointerType","Pointer","PointerEvent","MouseEvent","Mouse","dispatchEvent","withoutDefault","types","act","suppress","preventDefault","addEventListener","removeEventListener","clickOrRoute","matches","target","getAttribute","error","internal","focus","anchor","inAnchor","cancels","checkable","before","checked","click","Event","visible","details","summary","children","find","tagName","hidden","hasAttribute","style","getComputedStyle","display","visibility","menuShowing","expanded","id","menu","getElementById","openMenu","signal","statesItself","ms","Promise","resolve","started","tick","setTimeout","Observer","MutationObserver","globalThis","observer","records","some","r","addedNodes","disconnect","clearTimeout","timer","observe","documentElement","subtree","childList","addsContent","aborted","SEARCH_WORDS","performPageAction","refs","get","isConnected","fieldset","legend","isDisabled","isTypeable","readOnly","isReadOnly","token","isSecretField","HTMLInputElement","HTMLTextAreaElement","proto","prototype","getOwnPropertyDescriptor","set","call","textContent","setValue","attr","form","querySelectorAll","entersSearch","Keyboard","KeyboardEvent","code","keyCode","which","down","defaultPrevented","requestSubmit","pressEnter","err","Error","TRUNCATION_MARKER","EXPLICIT_ROLES","wrapsControls","child","actionableRole","explicit","tag","headingLevel","spacedText","childNodes","nodeType","isSecret","clean","NAMED_BY_CONTENT","accessibleName","role","labelledBy","fromAttr","label","getElementsByTagName","l","htmlFor","own","filter","querySelector","contents","placeholder","title","quote","states","disabledByFieldset","destination","loc","STORAGE_KEY","LEGACY_CHAT_ID_KEY","draftKey","loadDraft","sessionStorage","getItem","persistDraft","setItem","removeItem","DEFAULT_MAX_TOTAL_SIZE","useChat","agentSlug","pageContext","onRelativeLinkClick","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","pageActionControllerRef","streamDeltasRef","hasUsedToolsRef","conversationIdRef","isLoadingRef","pageContextRef","pageRefsRef","Map","onRelativeLinkClickRef","requestHeadersRef","approvalConversationIdRef","approvalDetachedRef","probedConversationRef","resumeDeadlineRef","creatingSessionRef","uploadAbortRef","AbortController","effectiveMaxFileCount","effectiveMaxTotalSize","getSteerUrl","steer","getApproveUrl","approve","getApproveUrlRef","getPendingApprovalsUrl","convId","readPendingApprovals","res","fetch","json","turnRunning","turn","reloadHistory","useCallback","getUploadUrl","upload","updateConversationId","ensureConversation","slug","sessionsUrl","sessions","promise","agent_slug","uploadSingleFile","file","uploadUrl","formData","FormData","append","uploadHeaders","fromEntries","entries","ids","file_ids","handleFileAdd","fileList","candidates","tempId","crypto","randomUUID","accepted","prev","currentCount","currentSize","reduce","sum","slotsAvailable","sizeLeft","filtered","newEntries","rawFile","uploadStatus","DOMException","answerPageAction","conversationIdHint","refuse","page_action","snapshot","result","quietMs","maxMs","quiet","done","cap","attributes","characterData","waitForDomSettle","page","snapshotError","maxChars","budget","truncated","walk","level","line","takePageSnapshot","handleNewChat","close","abort","useEffect","draftFlushRef","state","setInterval","catch","clearInterval","cancelled","canSteer","submitApprovalDecisions","decisions","d","decision","verdict","rejectionReason","rejection_reason","handlePaste","files","clipboardData","handleSendMessage","steerText","steerUrl","optimistic","timestamp","steerMessage","userMsg","assistantId","currentAssistantId","accumulated","doneReceived","deltas","apply","textChanged","thinking","frame","closed","cancelFrame","cancelAnimationFrame","flush","changed","chunk","schedule","requestAnimationFrame","createStreamDeltas","segId","controller","pageActions","fileIds","requestBody","opts","streaming","threadId","runId","context","forwardedProps","supportsToolApproval","supports_tool_approval","supportsPageActions","supports_page_actions","keys","serialized","buildRequestBody","parseEvent","getParser","reader","getReader","decoder","TextDecoder","buffer","ensureSegment","segmentId","read","decode","stream","pop","rawLine","jsonStr","parsed","elapsedStartMs","finalContent","completedAt","handleStopGenerating","handleSwitchConversation","STORAGE_AGENT_KEY","useAgents","agents","setAgents","agentsLoading","setAgentsLoading","agentsError","setAgentsError","selectedAgent","setSelectedAgent","agentMenuOpen","setAgentMenuOpen","agentsUrl","list","parseAgents","savedSlug","finally","handleSwitchAgent","agent","onSwitch","parseConversation","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","hasFocus","wasLoading","unfocused","away","viewingChat","showMessage","startTitleFlash","Notification","permission","notifyOS","useComposerExtras","prompts","setPrompts","quota","setQuota","restLike","promptsPath","quotaPath","promptsUrl","quotaUrl","rawList","description","parsePrompts","quotaNonce","setQuotaNonce","refreshQuota","q","period","scope","parseQuota","useAgentSuggestions","suggestions","setSuggestions","loading","setLoading","o","prompt","parseSuggestions","useShallowStable","is","every","hasOwnProperty","shallowEqual","useLatestCallback","callback","useInsertionEffect","stable","AlertTriangleIcon","className","_jsxs","xmlns","width","height","viewBox","fill","stroke","strokeWidth","strokeLinecap","strokeLinejoin","_jsx","ArrowRightLeftIcon","AttachFileIcon","BotIcon","x","y","rx","BrainIcon","CheckCircleIcon","cx","cy","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","SPEAKER","VolumeIcon","VolumeOffIcon","WrenchIcon","XCircleIcon","Dropdown","open","onClose","anchorRef","placement","panelRef","pos","setPos","top","stableOnClose","handler","active","listener","useClickOutside","useLayoutEffect","rect","getBoundingClientRect","panelHeight","offsetHeight","spaceBelow","innerHeight","bottom","spaceAbove","preferredLeft","min","innerWidth","portalTarget","createPortal","Spinner","tooltipMaxWidth","opening","maxRight","minLeft","Tooltip","tipRef","setOpening","setPlacement","below","shift","center","minTop","placeTooltip","panel","viewportWidth","tooltipOpening","onMouseEnter","onMouseLeave","onFocus","onBlur","maxWidth","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","showAgentSearch","CurrentModeIcon","onClick","autoFocus","onChange","onKeyDown","_Fragment","conv","isActive","when","opt","createDictationController","recognition","events","phase","startQueued","disposed","reported","isOn","report","onListeningChange","tryStart","start","onresult","finalText","interimText","resultIndex","results","isFinal","transcript","phrase","onFinal","onInterim","onerror","onend","toggle","stop","cancel","dispose","getSpeechRecognition","w","SpeechRecognition","webkitSpeechRecognition","SIZE","CIRCUMFERENCE","PI","ROWS","color","ContextUsageIndicator","usage","setOpen","ratio","pruning","compacting","ringColor","textColor","percent","counts","headline","tokens","rows","expandable","gauge","strokeDasharray","strokeDashoffset","transform","backgroundColor","borderRadius","PromptPicker","onPick","query","setQuery","QuotaIndicator","quotaPeriodLabel","withScope","compact","exhausted","nearLimit","barColor","ChatInput","onInputChange","onSend","onStop","onFileAdd","onFileRemove","onPaste","separatorColor","composerToolbar","fileInputRef","textareaRef","revealEndRef","dictation","onFinalText","listening","setListening","interim","setInterim","controllerRef","onFinalTextRef","supported","Ctor","continuous","interimResults","lang","navigator","language","useDictation","trimEnd","scrollHeight","scrollTop","hasToolbar","Boolean","isFileManagementEnabled","hasContent","hasFilesUploading","canSend","hasAttachments","showSteerSend","send","footerText","borderTopColor","borderTopWidth","multiple","shiftKey","maxHeight","disabled","feedbackKeyOf","msg","parser","unified","use","remarkParse","remarkGfm","freeze","SILENT","inlineText","collectLines","FENCED_BLOCK_RE","speechSupported","speechSynthesis","SpeechSynthesisUtterance","useSpeechReader","locale","speakingId","setSpeakingId","localeRef","synth","createUtterance","generation","finish","gen","speak","chunks","forEach","part","last","utterance","onEnd","onError","createSpeechController","documents","maxLength","sentences","pieces","sentence","word","piece","chunkSpeech","markdown","speakableText","FORMAT_OPTIONS","time","hour","minute","date","dateWithYear","year","full","dateStyle","timeStyle","formatters","formatter","kind","cached","Intl","DateTimeFormat","browserTimers","clear","handle","KEYS_TOWARD_TOP","singleTouchY","touches","clientY","SCROLLING_OVERFLOW","formatValue","useRevealIntoView","raf","scrollIntoView","behavior","block","inline","BUTTON_BASE","ApprovalCard","proposal","onDecide","rejecting","setRejecting","reason","setReason","reasonId","useId","rejectPanelRef","argumentNames","decide","properties","entry","argumentSchema","ChatApprovalPrompt","onSubmit","isSubmitting","setDecisions","submitted","sent","setSent","busy","submit","all","decidedCount","allDecided","willRunAlways","footerRef","rootRef","decided","total","ImageLightbox","hostRef","closeButtonRef","root","setRoot","previouslyFocused","activeElement","HTMLElement","preventScroll","stopPropagation","ChatImage","maxHeightClass","needsAuth","isRelativeUrl","blobUrl","setBlobUrl","errored","setErrored","setExpanded","objectUrl","blob","createObjectURL","revokeObjectURL","displaySrc","PLAY_CLASS","ChatWaitingPanel","onPlay","playUrl","keepEndInView","defaultMessages","reducedMotion","reduced","setReduced","matchMedia","mql","addListener","removeListener","usePrefersReducedMotion","msgIndex","setMsgIndex","animation","rel","delegatingLabel","pollingLabel","collectingLabel","cleanReasoningText","ThinkingTextBubble","isOverflowing","setIsOverflowing","clientHeight","formatElapsed","seconds","useSustained","delayMs","firedKey","setFiredKey","ChatThinking","miniGameEnabled","onPlayWaitingGame","waitingGameUrl","StatusIcon","showDots","rawNames","lower","toUpperCase","unique","consultName","targetName","resolveStatusVisual","nowMs","setNowMs","showElapsed","reasoningText","prose","end","cut","reverse","paragraph","reasoningWindowText","showReasoning","waitedWithoutReasoning","reasoningWentQuiet","showWaiting","delay","blockParser","holdsDefinition","lineStart","lastIndexOf","settlesBefore","endsIndented","MarkdownBlockSplitter","this","previous","blocks","starts","settled","firstDefinition","single","reuse","position","splitMarkdownBlocks","MarkdownRenderContext","createContext","copiedBlock","onCopyCode","MARKDOWN_COMPONENTS","pre","useContext","codeStr","ul","ol","li","strong","em","hr","blockquote","internalHref","routeInternally","openInNewTab","isRelativeHref","img","h1","h2","h3","table","tr","th","td","REMARK_PLUGINS","remarkBreaks","MarkdownBody","memo","Markdown","remarkPlugins","urlTransform","components","displayName","StreamedMarkdownBody","splitter","MarkdownMessage","setCopiedBlock","byBlocks","setByBlocks","processedContent","clipboard","writeText","renderContext","Provider","prettyTraceValue","toolDisplayName","ToolCallRow","inputDisplay","outputDisplay","hasInput","ReasoningDetailsDialog","dialogRef","dialog","focusable","first","totalCalls","transfers","trace","summaryParts","tn","parentOf","overflowYOf","overflowY","isControl","Element","fileExtensionLabel","dot","ext","IMAGE_EXTENSIONS","SHOWN_ON_HOVER","FOOTER_BUTTON","ACCENT_ON_HOVER","REVEALED_BUTTON","DIMMED_BUTTON","ACTIVE_BUTTON","MessageTime","getFullYear","getMonth","getDate","format","toISOString","formatMessageTime","dateTime","MessageCopyButton","copied","setCopied","MessageFeedbackButtons","locked","pending","downRef","lockedId","inert","button","Icon","FeedbackComment","onSave","onSkip","setText","canSave","ctrlKey","metaKey","ReadAloudButton","speaking","onToggle","MessageRow","isStreaming","onDownloadFile","resolveAttachmentUrl","feedbackMode","onRate","onSaveComment","onSkipComment","canSpeak","isSpeaking","onToggleSpeech","setShowReasoning","isAssistant","isEmpty","commentOpen","wasCommentOpen","renderAttachmentCard","att","previewUrl","isImageAttachment","isWorking","sizeLabel","bytes","renderFileChip","hasReasoningDetails","showActions","plainText","stripped","stripFileMarkers","speechDocuments","sources","answerMarkdownSources","shown","session","stored","speechKey","seen","add","buildUserFileBlocks","attByFileId","buildAssistantBlocks","timestampUnknown","ChatMessages","onMessageFeedback","onSubmitApprovalDecisions","scrollRef","renderWindow","setRenderWindow","feedbackEntries","rate","saveComment","skipComment","setState","latest","seqRef","put","patch","seq","change","notify","useMessageFeedback","speech","follower","getBox","timers","following","lastTop","smooth","jumpToEnd","box","scrollTo","stopWaiting","arrived","reveal","keepUp","onScroll","movedUp","movedDown","distance","leave","createScrollFollower","touchAnchorRef","leaveFrom","container","innerBoxScrollsUp","firstMessageId","userMessageCount","seenRef","thinkingLen","hasEarlierMessages","visibleMessages","stopSpeech","spokenStillShown","speechScope","streamingMessageId","awaitingApproval","tabIndex","onWheel","deltaY","abs","deltaX","wheelScrollsUp","currentTarget","altKey","keyScrollsUp","isContentEditable","onPointerDown","press","onControl","onContainer","clientWidth","thumbBottom","pressTakesScrollbar","clientX","clientLeft","clientTop","onTouchStart","onTouchMove","anchorY","swipeScrollsUp","nextTouchAnchor","isStreamingMessage","Greeting","firstName","after","hasSlot","at","translateAround","ChatWelcome","promptSuggestions","onPromptClick","agentDescription","suggestionsLoading","fontFamily","ConversationSidebar","collapsed","onToggleCollapsed","onSelect","onDelete","onRename","editingId","setEditingId","draftTitle","setDraftTitle","settledRef","commitRename","isEditing","ChatPanel","topOffset","apiEndpointsProp","user","accentColor","promptSuggestionsProp","draftBorderColor","resizable","onWidthChange","onResizeStart","onResizeEnd","disableFileManagement","onRelativeLinkClickProp","onDownloadError","onDownloadErrorProp","requestHeadersProp","pageContextProp","pushContentSelector","onPlayWaitingGameProp","notifyOnComplete","onTaskComplete","onTaskCompleteProp","onMessageFeedbackProp","disableImagePreviews","contextUsageEnabled","setModeMenuOpen","agentSuggestions","defaultSuggestions","refreshConversations","deleteConversation","renameConversation","setConversations","setConversationsLoading","history","useConversations","setHistoryMenuOpen","sidebarCollapsed","setSidebarCollapsed","showConversationSidebar","handleSelectConversation","handleDeleteConversation","sidebarWidth","handleResizeStart","defaultWidth","isResizing","setSidebarWidth","parseInt","setIsResizing","isResizingRef","sidebarWidthRef","onWidthChangeRef","onResizeEndRef","handleMouseMove","newWidth","clamped","handleMouseUp","cursor","userSelect","handleWindowResize","useSidebarResize","pushWidth","setProperty","contentElement","originalPaddingRight","paddingRight","originalTransition","transition","resolvedLogo","xtmOneUrl","conversationAgentName","setConversationAgentName","checkVisibility","getClientRects","downloadPathProvided","download","canDownload","handleDownloadFile","link","createElement","appendChild","remove","cssVars","isMountedRef","requestedConversationId","isStale","createdAt","sentAt","parseRestoredMessages","containerClasses","containerStyle","onMouseDown","_","ChatToggleButton","isOpen","icon","resolvedIcon","borderColor"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../src/utils/index.ts","../../src/utils/speech.ts","../../src/utils/routes.ts","../../src/i18n/es.ts","../../src/i18n/index.ts","../../src/i18n/fr.ts","../../src/i18n/ja.ts","../../src/utils/feedback.ts","../../src/utils/conversationRefs.ts","../../src/utils/workspaces.ts","../../src/utils/refusal.ts","../../src/utils/randomId.ts","../../src/hooks/protocols/parseRestEvent.ts","../../src/hooks/protocols/parseLegacyEvent.ts","../../src/hooks/protocols/parseAgUiEvent.ts","../../src/utils/sessionGate.ts","../../src/utils/pageActions.ts","../../src/utils/pageSnapshot.ts","../../src/utils/storage.ts","../../src/utils/sse.ts","../../src/utils/askHost.ts","../../src/utils/failedUpload.ts","../../src/utils/agentMentions.ts","../../src/hooks/usePanelRequests.ts","../../src/hooks/useChat.ts","../../src/hooks/streamDeltas.ts","../../src/hooks/useAgents.ts","../../src/hooks/useConversations.ts","../../src/hooks/useSidebarResize.ts","../../src/hooks/useAwayCompletionNotice.ts","../../src/hooks/useComposerExtras.ts","../../src/utils/quota.ts","../../src/hooks/useAgentSuggestions.ts","../../src/hooks/useStableProps.ts","../../src/hooks/dictationController.ts","../../src/hooks/useDictation.ts","../../src/hooks/voiceTurn.ts","../../src/hooks/voiceCallController.ts","../../src/hooks/useVoiceCall.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/ChevronRightIcon.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/FolderIcon.tsx","../../src/components/icons/FolderInputIcon.tsx","../../src/components/icons/FolderPlusIcon.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/MessageSquareIcon.tsx","../../src/components/icons/MicIcon.tsx","../../src/components/icons/PencilIcon.tsx","../../src/components/icons/PhoneOffIcon.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/VolumeIcon.tsx","../../src/components/icons/WrenchIcon.tsx","../../src/components/icons/XCircleIcon.tsx","../../src/components/ClearAllConversations.tsx","../../src/components/Dropdown.tsx","../../src/hooks/useClickOutside.ts","../../src/components/Spinner.tsx","../../src/utils/tooltipPlacement.ts","../../src/components/Tooltip.tsx","../../src/components/ChatHeader.tsx","../../src/hooks/useConversationReferenceSearch.ts","../../src/hooks/useMentionMenu.ts","../../src/components/ContextUsageIndicator.tsx","../../src/components/MentionMenu.tsx","../../src/components/PromptPicker.tsx","../../src/components/QuotaIndicator.tsx","../../src/components/ChatInput.tsx","../../src/hooks/useMessageFeedback.ts","../../src/hooks/useSpeechReader.ts","../../src/utils/messageTime.ts","../../src/utils/scrollFollow.ts","../../src/components/ArtifactCard.tsx","../../src/components/icons/PresentationIcon.tsx","../../src/components/icons/FileTextIcon.tsx","../../src/components/icons/ClapperboardIcon.tsx","../../src/utils/polling.ts","../../src/components/ChatApprovalPrompt.tsx","../../src/components/ChatImage.tsx","../../src/components/ChatWaitingPanel.tsx","../../src/components/ChatThinking.tsx","../../src/components/HostRenderBoundary.tsx","../../src/utils/markdownBlocks.ts","../../src/components/MarkdownMessage.tsx","../../src/components/ReasoningDetailsDialog.tsx","../../src/components/ChatMessages.tsx","../../src/components/ChatVoiceCall.tsx","../../src/components/ChatWelcome.tsx","../../src/components/ConversationSidebar.tsx","../../src/components/ChatPanel.tsx","../../src/utils/sessionEnd.ts","../../src/hooks/useWorkspaces.ts","../../src/components/ChatToggleButton.tsx"],"sourcesContent":["import type { ChatArtifact, ChatArtifactKind, Translate } from '../types';\n\nexport 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 // Find the outer block's *matching* closer rather than the last bare fence in\n // the message. Same-length fences are ambiguous — an inner ``` looks exactly\n // like the outer closer — so inner fences are paired off with two structural\n // signals:\n // • a labelled fence (```lang) opens a nested block, and the next bare\n // fence closes it (depth tracking); and\n // • two consecutive *bare* fences wrapping a non-blank body form an\n // unlabelled nested code block — a ``` … ``` example inside the document.\n // A bare fence at depth 0 is the outer closer when the next fence is\n // labelled, absent, or bare-but-separated-by-only-blank-lines: that last case\n // is a block boundary (this closer followed by a *separate* block), not\n // nesting, so the separate block is never absorbed.\n //\n // Taking the last bare fence instead swallowed everything up to it — trailing\n // prose and unrelated code blocks alike — whenever a later block followed the\n // markdown one.\n const fences: { idx: number; run: number; bare: boolean }[] = [];\n for (let i = openerIdx + 1; i < lines.length; i++) {\n const m = lines[i].match(fenceRe);\n if (m) fences.push({ idx: i, run: m[2].length, bare: m[3].trim() === '' });\n }\n\n // True when every line strictly between two fence lines is blank — which\n // separates a nested code block (real body) from a mere block boundary.\n const blankBetween = (a: number, b: number): boolean => {\n for (let k = a + 1; k < b; k++) {\n if (lines[k].trim() !== '') return false;\n }\n return true;\n };\n\n let depth = 0;\n let maxRun = 3;\n let nestedCount = 0;\n let closerIdx = -1;\n for (let j = 0; j < fences.length; j++) {\n const f = fences[j];\n if (f.bare && depth === 0) {\n const next = fences[j + 1];\n if (!next || !next.bare || blankBetween(f.idx, next.idx)) {\n closerIdx = f.idx;\n break;\n }\n depth++; // opens an unlabelled nested code block\n } else {\n // A labelled fence opens a nested block; a bare fence at depth > 0 closes\n // the innermost one.\n depth = f.bare ? Math.max(0, depth - 1) : depth + 1;\n }\n nestedCount++;\n maxRun = Math.max(maxRun, f.run);\n }\n\n // Only harden when the block really contains nested fences AND its own closer\n // was found — otherwise leave the text alone rather than extending the block\n // over whatever follows.\n if (closerIdx === -1 || 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 const cm = lines[closerIdx].match(fenceRe)!;\n lines[closerIdx] = `${cm[1]}${fence}`;\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/**\n * The text the panel hands to its markdown renderer. Order matters — image\n * alt-text is flattened before anything else looks at line structure, and the\n * JSON wrap must see the raw payload before fences are hardened.\n */\nexport function prepareMarkdown(content: string): string {\n return hardenNestedCodeFences(normalizeMarkdownTables(wrapBareJson(normalizeImageMarkdown(content))));\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: Translate = (key) => key;\n\n/**\n * Translates `key`, then splices `values` into the result's `{name}` slots.\n *\n * The point is that the key stays a whole sentence. Building a phrase out of\n * translated fragments — \"Waiting for\" + count + \"tasks\" — freezes English word\n * order and English plural rules into the layout, and a translator sees each\n * fragment with no sentence around it. Here the locale owns the whole sentence\n * and decides where the number goes; the host's `t` still does nothing but look\n * a key up, so this needs no support from it.\n *\n * Plurals stay two explicit keys (`'1 tool call'` / `'{count} tool calls'`) for\n * the same reason: a lookup cannot select a plural form, so the call site picks\n * the sentence and the locale translates it whole.\n */\nexport function translate(t: Translate, key: string, values: Record<string, string | number>): string {\n // One pass over the sentence rather than one pass per value: a value that\n // happens to contain `{something}` is then never mistaken for a slot of its\n // own. A slot with no value keeps its braces, so a stray one is visible\n // rather than silently blank.\n return t(key).replace(/\\{(\\w+)\\}/g, (slot, name) => (Object.hasOwn(values, name) ? String(values[name] ?? '') : slot));\n}\n\n/**\n * Same as `translate`, but leaves one slot unfilled and returns the text on\n * either side of it — for the case where the value has to be markup (a coloured\n * agent name, a link) and so cannot be spliced into a string.\n *\n * Without this the sentence gets cut into \"How can \" + node + \" help you, \",\n * which is untranslatable in any language that would not put the node in that\n * spot. Here the locale still owns the whole sentence and decides where the slot\n * sits.\n *\n * `hasSlot` is false for a translation that dropped the slot — a locale that\n * reworded the sentence so it no longer names the value. The caller then renders\n * the sentence without the node instead of letting it dangle off the end.\n */\nexport function translateAround(\n t: Translate,\n key: string,\n slot: string,\n values: Record<string, string | number> = {},\n): { before: string; after: string; hasSlot: boolean } {\n const sentence = translate(t, key, values);\n const token = `{${slot}}`;\n const at = sentence.indexOf(token);\n if (at === -1) return { before: sentence, after: '', hasSlot: false };\n return { before: sentence.slice(0, at), after: sentence.slice(at + token.length), hasSlot: true };\n}\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: Translate): 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 translate(t, '{count}m ago', { count: minutes });\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return translate(t, '{count}h ago', { count: hours });\n const days = Math.floor(hours / 24);\n if (days < 7) return translate(t, '{count}d ago', { count: days });\n return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });\n}\n\n/**\n * A complete marker an agent embeds in prose: a file (`[[FILE:<id>]]`), or a\n * deck, report or video it made (`[[PRESENTATION:<id>]]`, optionally naming a\n * slide as `#/<slide id>`, `[[REPORT:<id>]]`, `[[VIDEO:<id>]]`). The backend\n * matches the names case-insensitively, and so does this.\n */\nconst BUILT_IN_MARKERS = ['FILE', 'PRESENTATION', 'REPORT', 'VIDEO'] as const;\nconst BUILT_IN_MARKER_SOURCE = `(${BUILT_IN_MARKERS.join('|')}):([^\\\\]#]+)(?:#\\\\/([A-Za-z0-9_-]{1,64}))?`;\n\n/** A host marker name: upper-case letters, digits and underscores. */\nconst MARKER_NAME = /^[A-Z][A-Z0-9_]*$/;\n\n/** The host's marker names, upper-cased, valid and deduplicated. */\nexport function markerKinds(names: Iterable<string> | undefined): string[] {\n const out = new Set<string>();\n for (const name of names ?? []) {\n const upper = name.toUpperCase();\n if (MARKER_NAME.test(upper)) out.add(upper);\n }\n return [...out].sort();\n}\n\nconst escapeRe = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\ninterface MarkerGrammar {\n complete: RegExp;\n partial: RegExp;\n heads: string[];\n longestHead: number;\n}\n\nconst grammars = new Map<string, MarkerGrammar>();\n/** Grammars kept, one per set of host kinds. */\nconst MAX_GRAMMARS = 32;\n\n/**\n * The marker grammar for the built-in names plus *hostKinds*. A host name\n * takes everything up to `]]` as its arguments; one that is also built in is\n * read with the built-in grammar and handed over whole.\n *\n * The partial pattern matches an INCOMPLETE marker at the very end of the\n * string: SSE streams can split a marker before the closing `]]` arrives, so\n * the tail may be `[[FILE`, `[[VIDEO:`, `[[REPORT:abc` or `[[FILE:abc]`. It is\n * anchored on a whole marker name, vanishingly unlikely to end real prose, so\n * it never clips real content.\n */\nfunction markerGrammar(hostKinds: readonly string[]): MarkerGrammar {\n const extra = hostKinds.filter((k) => !(BUILT_IN_MARKERS as readonly string[]).includes(k));\n const key = extra.join(',');\n const cached = grammars.get(key);\n if (cached) return cached;\n const names = [...BUILT_IN_MARKERS, ...extra].map(escapeRe).join('|');\n const hostSource = extra.length > 0 ? `|(${extra.map(escapeRe).join('|')}):([^\\\\]]+)` : '';\n const heads = [...BUILT_IN_MARKERS, ...extra].map((name) => `[[${name}:`);\n const grammar = {\n complete: new RegExp(`\\\\[\\\\[(?:${BUILT_IN_MARKER_SOURCE}${hostSource})\\\\]\\\\]`, 'gi'),\n partial: new RegExp(`\\\\[\\\\[(?:${names})(?::[^\\\\]]*)?\\\\]?$`, 'i'),\n heads,\n longestHead: Math.max(...heads.map((head) => head.length)),\n };\n // A host whose kinds keep changing would grow the cache without bound: the\n // oldest grammar goes first.\n if (grammars.size >= MAX_GRAMMARS) grammars.delete(grammars.keys().next().value as string);\n grammars.set(key, grammar);\n return grammar;\n}\n\n/**\n * *text* without a trailing start of a marker name (`[`, `[[`, `[[PRES`). Only\n * for an answer still streaming, where the next chunk may complete it: once\n * the answer is done, the same text is prose.\n */\nfunction withoutMarkerHead(text: string, grammar: MarkerGrammar): string {\n for (let i = Math.max(0, text.length - grammar.longestHead); i < text.length; i++) {\n if (text[i] !== '[') continue;\n const tail = text.slice(i).toUpperCase();\n if (grammar.heads.some((head) => head.startsWith(tail))) return text.slice(0, i);\n }\n return text;\n}\n\n/**\n * Strip the markers an agent embeds in its reply to point at the files, decks,\n * reports and videos it made (and the host's own, *hostKinds*). They render as\n * cards of their own, so the raw markers must be removed from the prose.\n * Complete markers are removed anywhere; an incomplete marker at the end is\n * also removed so a partially streamed token never flickers as raw\n * `[[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 stripAnswerMarkers(content: string, hostKinds: readonly string[] = []): string {\n if (!content) return content;\n const grammar = markerGrammar(hostKinds);\n // `replace` with a global pattern starts from the beginning, whatever `lastIndex` holds.\n const stripped = content.replace(grammar.complete, '').replace(grammar.partial, '');\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, a file marker, an artifact marker or a host marker. */\nexport type AnswerPart =\n | { type: 'text'; value: string }\n | { type: 'file'; fileId: string }\n | { type: 'artifact'; kind: ChatArtifactKind; id: string; slideRef?: string }\n | { type: 'marker'; kind: string; args: string };\n\n/**\n * Split assistant content into ordered parts around complete markers, so the\n * renderer can place each card at its marker's source position (preserving\n * interleaved order). An artifact id is lowercased, the form the backend\n * describes it in. A marker named in *hostKinds* is a `marker` part with its\n * arguments as written, a built-in one included. A trailing incomplete marker\n * (an SSE token split mid-stream) is removed from the final text part so it\n * never shows as raw `[[FILE:...` text, and so is, while *streaming*, a chunk\n * that stopped inside a marker's name (`[[PRES`).\n */\nexport function splitAnswerMarkers(content: string, streaming = false, hostKinds: readonly string[] = []): AnswerPart[] {\n if (!content) return [];\n const grammar = markerGrammar(hostKinds);\n const parts: AnswerPart[] = [];\n let lastIndex = 0;\n // `matchAll` walks a copy of the cached pattern: no state of its own to reset.\n for (const match of content.matchAll(grammar.complete)) {\n if (match.index > lastIndex) {\n parts.push({ type: 'text', value: content.slice(lastIndex, match.index) });\n }\n if (match[1]) {\n const kind = match[1].toUpperCase();\n if (hostKinds.includes(kind)) {\n parts.push({ type: 'marker', kind, args: match[3] ? `${match[2]}#/${match[3]}` : match[2] });\n } else if (kind === 'FILE') {\n parts.push({ type: 'file', fileId: match[2] });\n } else {\n parts.push({ type: 'artifact', kind: kind.toLowerCase() as ChatArtifactKind, id: match[2].toLowerCase(), slideRef: match[3] || undefined });\n }\n } else {\n parts.push({ type: 'marker', kind: match[4].toUpperCase(), args: match[5] });\n }\n lastIndex = match.index + match[0].length;\n }\n const rest = content.slice(lastIndex).replace(grammar.partial, '');\n const tail = streaming ? withoutMarkerHead(rest, grammar) : rest;\n if (tail) parts.push({ type: 'text', value: tail });\n return parts;\n}\n\n/**\n * The markdown documents an assistant answer is drawn from, in order: each\n * prose part between its markers (`splitAnswerMarkers`), prepared as\n * `MarkdownMessage` prepares it (`prepareMarkdown`). The transcript parses each\n * one as a document of its own, and so must anything that reads the answer\n * (read aloud) to read what the screen shows.\n */\nexport function answerMarkdownSources(content: string, hostKinds: readonly string[] = []): string[] {\n const sources: string[] = [];\n for (const part of splitAnswerMarkers(content, false, hostKinds)) {\n if (part.type === 'text' && part.value.trim()) sources.push(prepareMarkdown(part.value));\n }\n return sources;\n}\n\n/** `kind:id`, how an artifact marker finds the descriptor it renders. */\nexport const artifactKey = (kind: string, id: string): string => `${kind}:${id.toLowerCase()}`;\n\n/** A video whose newest render has not finished: its card follows it. */\nexport const isRenderInProgress = (artifact: ChatArtifact): boolean =>\n artifact.kind === 'video' && (artifact.status === 'pending' || artifact.status === 'running');\n\n/** The kind's name, for a card with no title and for its accessible label. */\nexport function artifactKindLabel(kind: ChatArtifactKind, t: Translate): string {\n if (kind === 'presentation') return t('Presentation');\n if (kind === 'report') return t('Report');\n return t('Video');\n}\n\n/** The facts line under a card's title: its slides, its sections, or its scenes and length. */\nexport function artifactFacts(artifact: ChatArtifact, t: Translate): string {\n const parts: string[] = [];\n if (artifact.kind === 'presentation' && artifact.slideCount !== undefined) {\n parts.push(translate(t, '{count} slides', { count: artifact.slideCount }));\n }\n if (artifact.kind === 'report' && artifact.sectionCount !== undefined) {\n parts.push(translate(t, '{count} sections', { count: artifact.sectionCount }));\n }\n if (artifact.kind === 'video') {\n if (artifact.sceneCount !== undefined) parts.push(translate(t, '{count} scenes', { count: artifact.sceneCount }));\n const duration = formatVideoDuration(artifact.durationMs);\n if (duration) parts.push(duration);\n }\n return parts.join(' · ');\n}\n\n/** What a video's render is doing, when the card has something to say about it. */\nexport function videoRenderLabel(artifact: ChatArtifact, t: Translate): string | undefined {\n if (artifact.kind !== 'video') return undefined;\n if (artifact.status === 'running') {\n return translate(t, 'Rendering {progress}%', { progress: Math.round(artifact.progress ?? 0) });\n }\n if (artifact.status === 'pending') return t('Waiting to render');\n if (artifact.downloads.length > 0) return undefined;\n if (artifact.status === 'failed' || artifact.status === 'cancelled') return t('The render did not finish');\n return t('Not rendered yet');\n}\n\n/** The label of a download button. */\nexport function artifactDownloadLabel(format: string, t: Translate): string {\n if (format === 'pdf') return 'PDF';\n if (format === 'mp4') return 'MP4';\n if (format === 'pptx') return t('PowerPoint');\n if (format === 'pptx_native') return t('PowerPoint (editable)');\n return format.toUpperCase();\n}\n\n/** The XTM One page of a card, linked only when it is http(s). */\nexport function artifactLink(artifact: ChatArtifact): string | undefined {\n if (!artifact.url) return undefined;\n try {\n const url = new URL(artifact.url);\n return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** The extension a download in *format* is saved with. */\nexport function artifactFileExtension(format: string): string {\n return format === 'pptx_native' ? 'pptx' : format;\n}\n\n/** `m:ss`, the length of a video; `undefined` for nothing to show. */\nexport function formatVideoDuration(ms: number | undefined): string | undefined {\n if (!ms || ms <= 0) return undefined;\n const total = Math.round(ms / 1000);\n return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;\n}\n\n/**\n * The file name a `Content-Disposition` header names: the RFC 5987\n * `filename*=UTF-8''...` form first, else the quoted `filename=\"...\"`.\n */\nexport function filenameFromContentDisposition(header: string | null | undefined): string | undefined {\n if (!header) return undefined;\n const extended = /filename\\*\\s*=\\s*(?:UTF-8|utf-8)''([^;]+)/.exec(header);\n if (extended) {\n try {\n return decodeURIComponent(extended[1].trim().replace(/^\"|\"$/g, '')) || undefined;\n } catch {\n // A malformed escape: fall back to the plain name.\n }\n }\n const plain = /filename\\s*=\\s*(\"([^\"]*)\"|[^;]+)/.exec(header);\n const name = plain ? (plain[2] ?? plain[1]).trim() : '';\n return name || undefined;\n}\n\n/**\n * How an agent's `icon` shows: an image URL as an image, an emoji as text, and\n * anything else (an icon library's name the panel cannot draw) not at all.\n */\nexport function agentIconKind(icon: string | null | undefined): 'image' | 'emoji' | null {\n const value = icon?.trim();\n if (!value) return null;\n if (/^(https:\\/\\/|data:image\\/)/i.test(value)) return 'image';\n if (value.length <= 8 && !/[\\p{L}\\p{N}]/u.test(value)) return 'emoji';\n return null;\n}\n\n/** Agents under their `group` heading, in first-seen order; the ungrouped first, under none. */\nexport function groupAgents<T extends { group?: string }>(agents: readonly T[]): { group: string | null; agents: T[] }[] {\n const groups = new Map<string | null, T[]>();\n for (const agent of agents) {\n const key = agent.group?.trim() || null;\n if (!groups.has(key)) groups.set(key, []);\n groups.get(key)!.push(agent);\n }\n const ungrouped = groups.get(null);\n groups.delete(null);\n return [...(ungrouped ? [{ group: null, agents: ungrouped }] : []), ...[...groups].map(([group, list]) => ({ group, agents: list }))];\n}\n","import remarkGfm from 'remark-gfm';\nimport remarkParse from 'remark-parse';\nimport { unified } from 'unified';\n\n/**\n * Reading an answer aloud through the browser's speech synthesis.\n *\n * What is spoken is the text a reader sees, never the markdown that draws it:\n * no `**`, no `#`, no table pipes, no code block, no image, and no bare URL\n * spelled out character by character. The text is parsed with the parser the\n * panel renders with (remark-parse + remark-gfm), so what counts as a heading,\n * a list or a link is what the screen shows. It reads the documents the\n * transcript renders (`answerMarkdownSources`), never the raw answer: the\n * renderer splits it at its file markers and repairs it first (`prepareMarkdown`).\n *\n * Kept free of local imports so `node --test` runs it as is.\n */\n\nconst parser = unified().use(remarkParse).use(remarkGfm).freeze();\n\ntype MdNode = { type: string; value?: string; url?: string; children?: MdNode[] };\n\n/** Blocks and inline nodes a listener has nothing to hear in. */\nconst SILENT = new Set(['code', 'html', 'image', 'imageReference', 'definition', 'footnoteDefinition', 'footnoteReference', 'thematicBreak']);\n\nfunction inlineText(node: MdNode): string {\n if (SILENT.has(node.type)) return '';\n if (node.type === 'text' || node.type === 'inlineCode') return node.value ?? '';\n if (node.type === 'break') return ' ';\n const text = (node.children ?? []).map(inlineText).join('');\n // A link written as its own address (a bare URL, an autolink) is noise when\n // read out; a link with words says those words.\n if (node.type === 'link' && node.url && (text === node.url || `mailto:${text}` === node.url)) return '';\n return text;\n}\n\nfunction collectLines(node: MdNode, lines: string[]): void {\n if (SILENT.has(node.type)) return;\n switch (node.type) {\n case 'paragraph':\n case 'heading':\n lines.push(inlineText(node));\n return;\n case 'tableRow':\n lines.push(\n (node.children ?? [])\n .map(inlineText)\n .map((cell) => cell.trim())\n .filter(Boolean)\n .join(', '),\n );\n return;\n default:\n for (const child of node.children ?? []) collectLines(child, lines);\n }\n}\n\n/**\n * The words of an answer's markdown documents, one sentence-ending line per\n * block: a heading or a list item without its own punctuation gets a full\n * stop, so the voice pauses where the eye does. Each document is parsed on its own.\n */\nexport function speakableText(documents: readonly string[]): string {\n const lines: string[] = [];\n for (const markdown of documents) {\n if (markdown.trim()) collectLines(parser.parse(markdown) as MdNode, lines);\n }\n return lines\n .map((line) =>\n line\n .replace(/\\s+/g, ' ')\n .replace(/ ([.,!?;:])/g, '$1')\n .trim(),\n )\n .filter((line) => /[\\p{L}\\p{N}]/u.test(line))\n .map((line) => (/[.!?;:\\u3002\\uff01\\uff1f]$/.test(line) ? line : `${line}.`))\n .join('\\n');\n}\n\n/**\n * A fenced code block: a run of 3+ backticks or tildes, closed by a bare run of\n * the same character at least as long (`prepareMarkdown` lengthens the fence\n * around nested ones), or by the end of an answer cut short.\n */\nconst FENCED_BLOCK_RE = /^[ \\t]*((`|~)\\2{2,})[\\s\\S]*?(?:^[ \\t]*\\1\\2*[ \\t]*$|(?![\\s\\S]))/gm;\n\n/**\n * Whether an answer's markdown documents have words to read outside their code\n * blocks - what decides that it gets a \"Read aloud\" button. A cheap look rather\n * than `speakableText`: it runs for every message on screen, the full parse only on a click.\n */\nexport function hasSpeakableWords(documents: readonly string[]): boolean {\n return documents.some((markdown) => /[\\p{L}\\p{N}]/u.test(markdown.replace(FENCED_BLOCK_RE, '')));\n}\n\n/**\n * Longest utterance handed to the synthesiser. Chrome stops a single utterance\n * after roughly fifteen seconds without firing `end`, so a long answer is\n * queued as a series of short ones, cut between sentences.\n */\nexport const SPEECH_CHUNK_MAX_LENGTH = 220;\n\n/** `text` cut into utterances of at most `maxLength` characters, between sentences first, then between words. */\nexport function chunkSpeech(text: string, maxLength: number = SPEECH_CHUNK_MAX_LENGTH): string[] {\n const sentences = text\n .split(/\\n+|(?<=[.!?;:\\u3002\\uff01\\uff1f])\\s+/)\n .map((s) => s.trim())\n .filter(Boolean);\n const pieces: string[] = [];\n for (const sentence of sentences) {\n if (sentence.length <= maxLength) {\n pieces.push(sentence);\n continue;\n }\n let current = '';\n for (const word of sentence.split(' ')) {\n for (let start = 0; start < word.length; start += maxLength) {\n const part = word.slice(start, start + maxLength);\n if (!current) current = part;\n else if (current.length + 1 + part.length <= maxLength) current = `${current} ${part}`;\n else {\n pieces.push(current);\n current = part;\n }\n }\n }\n if (current) pieces.push(current);\n }\n const chunks: string[] = [];\n for (const piece of pieces) {\n const last = chunks.length - 1;\n if (last >= 0 && chunks[last].length + 1 + piece.length <= maxLength) chunks[last] = `${chunks[last]} ${piece}`;\n else chunks.push(piece);\n }\n return chunks;\n}\n\n/** The host's speech route (`apiEndpoints.tts`), and the headers it is called with. */\nexport interface TtsRoute {\n url: string;\n requestHeaders?: Record<string, string>;\n}\n\n/** The audio the host's speech route makes of *text*; throws when the route fails. */\nexport async function fetchSpeech(fetchImpl: typeof fetch, route: TtsRoute, text: string, signal: AbortSignal): Promise<Blob> {\n const res = await fetchImpl(route.url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(route.requestHeaders ?? {}) },\n body: JSON.stringify({ text, format: 'mp3' }),\n signal,\n });\n if (!res.ok) throw new Error(`TTS failed: ${res.status}`);\n return res.blob();\n}\n\n/**\n * The audio the host's speech route makes of each of *chunks*, in order, each\n * requested once the one before it is handed over: one request ahead of what\n * plays, never the whole answer at once (a rate-limited route would refuse the\n * later ones). `null` for a chunk the route failed.\n */\nexport async function* speechAhead(\n fetchImpl: typeof fetch,\n route: TtsRoute,\n chunks: readonly string[],\n signal: AbortSignal,\n): AsyncGenerator<Blob | null, void, undefined> {\n const request = (i: number) => fetchSpeech(fetchImpl, route, chunks[i], signal).catch(() => null);\n let next = chunks.length > 0 ? request(0) : null;\n for (let i = 0; next; i += 1) {\n const blob = await next;\n next = i + 1 < chunks.length && !signal.aborted ? request(i + 1) : null;\n yield blob;\n }\n}\n\n/** The part of `window.speechSynthesis` the controller uses. */\nexport interface SpeechSynthesisLike<U> {\n speak(utterance: U): void;\n cancel(): void;\n}\n\n/** Builds one utterance; `window.SpeechSynthesisUtterance` in the browser. */\nexport type CreateUtterance<U> = (text: string, options: { lang?: string; onEnd: () => void; onError: () => void }) => U;\n\nexport interface SpeechController {\n /**\n * Reads `chunks` as the message `id`, stopping whatever was being read.\n * Returns false, and reads nothing, when there is nothing to read.\n */\n speak(id: string, chunks: string[], lang?: string): boolean;\n /** Stops reading. A no-op when nothing is being read. */\n stop(): void;\n /** The message being read, or null. */\n readonly speakingId: string | null;\n}\n\n/**\n * One reader for the whole transcript: starting a message stops the previous\n * one, and `onChange` reports which message is being read (null once it ended,\n * failed or was stopped). The synthesiser reports the end of an utterance\n * asynchronously and, on `cancel()`, fails every queued one: each reading\n * carries a generation, and only the current one's events count.\n */\nexport function createSpeechController<U>(\n synth: SpeechSynthesisLike<U>,\n createUtterance: CreateUtterance<U>,\n onChange: (id: string | null) => void,\n): SpeechController {\n let generation = 0;\n let speakingId: string | null = null;\n\n const finish = (gen: number) => {\n if (gen !== generation || speakingId === null) return;\n generation += 1;\n speakingId = null;\n onChange(null);\n };\n\n // Nothing of ours being read, nothing is cancelled: the synthesiser is the\n // page's, and the host may be using it.\n const stop = () => {\n if (speakingId === null) return;\n generation += 1;\n speakingId = null;\n synth.cancel();\n onChange(null);\n };\n\n const speak = (id: string, chunks: string[], lang?: string) => {\n stop();\n const parts = chunks.filter((c) => c.trim());\n if (!parts.length) return false;\n const gen = generation;\n speakingId = id;\n onChange(id);\n parts.forEach((part, index) => {\n const last = index === parts.length - 1;\n const utterance = createUtterance(part, {\n lang,\n onEnd: () => {\n if (last) finish(gen);\n },\n onError: () => {\n if (gen !== generation) return;\n // Ends the reading before cancelling the rest of its queue, whose\n // failures then belong to a past generation.\n finish(gen);\n synth.cancel();\n },\n });\n synth.speak(utterance);\n });\n return true;\n };\n\n return {\n speak,\n stop,\n get speakingId() {\n return speakingId;\n },\n };\n}\n","import type { ApiEndpoints, BackendType } from '../types';\n\n/** The routes of `ApiEndpoints` given as a path. */\ntype RouteKey = { [K in keyof ApiEndpoints]-?: NonNullable<ApiEndpoints[K]> extends string ? K : never }[keyof ApiEndpoints];\n\n/**\n * The URL of a route the host opted into (`apiEndpoints[key]`, which has no\n * default path), or null: the host names none, or the backend has no per-path\n * routing (`singleEndpoint`, or a backend other than REST).\n */\nexport function optInRoute(apiBaseUrl: string, apiEndpoints: ApiEndpoints | undefined, backendType: BackendType, key: RouteKey): string | null {\n const path = apiEndpoints?.[key];\n if (backendType !== 'rest' || apiEndpoints?.singleEndpoint || typeof path !== 'string' || !path) return null;\n return `${apiBaseUrl}${path}`;\n}\n","import type { Catalog } from './index';\n\nconst es: Catalog = {\n '(no output)': '(sin salida)',\n '1 tool call': '1 llamada a una herramienta',\n '1 transfer': '1 transferencia',\n 'A file failed to upload: remove it, or send once the answer ends': 'Un archivo no se pudo subir: quítelo, o envíe cuando termine la respuesta',\n 'A message references at most {count} conversations': 'Un mensaje puede hacer referencia a {count} conversaciones como máximo',\n 'Acting on the page…': 'Actuando en la página…',\n 'Agent switching is not available here': 'El cambio de agente no está disponible aquí',\n 'Almost there': 'Ya casi estamos',\n 'Also applies to your scheduled runs, until you revoke it': 'También se aplica a sus ejecuciones programadas, hasta que lo revoque',\n 'Always allowed': 'Siempre permitido',\n 'Analyzing results…': 'Analizando los resultados…',\n 'Analyzing the details': 'Analizando los detalles',\n Approved: 'Aprobado',\n 'Ask Assistant': 'Preguntar al asistente',\n 'Ask a question...': 'Haga una pregunta...',\n Assistant: 'Asistente',\n 'Attach files': 'Adjuntar archivos',\n 'Attached files': 'Archivos adjuntos',\n 'Attachments wait for the current response': 'Los archivos adjuntos esperan la respuesta actual',\n Back: 'Volver',\n 'Bad response': 'Respuesta incorrecta',\n 'Browse agents': 'Explorar agentes',\n Cancel: 'Cancelar',\n 'Cancel question': 'Cancelar la pregunta',\n 'Clear all conversations': 'Borrar todas las conversaciones',\n Close: 'Cerrar',\n 'Collecting results from one task…': 'Recopilando los resultados de una tarea…',\n 'Collecting results from {count} tasks…': 'Recopilando los resultados de {count} tareas…',\n 'Composing answer…': 'Redactando la respuesta…',\n Confirm: 'Confirmar',\n 'Connecting the dots': 'Atando cabos',\n 'Consulting the sources': 'Consultando las fuentes',\n 'Consulting {agent}…': 'Consultando a {agent}…',\n 'Context full — older turns are being dropped': 'Contexto completo: se están eliminando los turnos más antiguos',\n 'Context nearly full — older turns are being summarized': 'El contexto está casi lleno: se están resumiendo los turnos más antiguos',\n 'Context used': 'Contexto utilizado',\n 'Continue now': 'Continuar ahora',\n Conversation: 'Conversación',\n 'Conversation history': 'Historial de conversaciones',\n 'Conversation title': 'Título de la conversación',\n 'Conversation transcript': 'Hilo de la conversación',\n Copied: 'Copiado',\n 'Copied!': '¡Copiado!',\n 'Copy code': 'Copiar código',\n 'Copy message': 'Copiar mensaje',\n 'Copy response': 'Copiar respuesta',\n 'Could not reach the assistant service. Check the connection and try again.':\n 'No se ha podido conectar con el servicio del asistente. Compruebe la conexión e inténtelo de nuevo.',\n 'Could not send your decision. Please try again.': 'No se ha podido enviar su decisión. Inténtelo de nuevo.',\n 'Create agent': 'Crear agente',\n 'Crunching the data': 'Procesando los datos',\n 'Decline this call': 'Rechazar esta llamada',\n Declined: 'Rechazado',\n 'Delegating one task…': 'Delegando una tarea…',\n 'Delegating {count} tasks…': 'Delegando {count} tareas…',\n 'Delete all conversations': 'Eliminar todas las conversaciones',\n 'Delete all conversations?': '¿Eliminar todas las conversaciones?',\n 'Delete conversation': 'Eliminar conversación',\n 'Delete workspace': 'Eliminar espacio de trabajo',\n 'Delete?': '¿Eliminar?',\n 'Dictate a message': 'Dictar un mensaje',\n 'Did not answer': 'No respondió',\n Download: 'Descargar',\n 'Download {format}': 'Descargar en {format}',\n 'Drop a conversation here, or start one with +.': 'Suelte aquí una conversación o inicie una con +.',\n 'Drop files to attach them': 'Suelte los archivos para adjuntarlos',\n 'End voice call': 'Finalizar la llamada de voz',\n 'Enter to send now · Esc to stop': 'Pulse Intro para enviar ahora · Esc para detener',\n 'Enter to send when the answer ends · Esc to stop': 'Pulse Intro para enviar cuando termine la respuesta · Esc para detener',\n 'Every conversation is in a workspace.': 'Todas las conversaciones están en un espacio de trabajo.',\n 'Expand image': 'Ampliar imagen',\n 'Failed to get a response. Resuming…': 'No se obtuvo respuesta. Reanudando…',\n 'Feedback already submitted': 'Valoración ya enviada',\n 'Files uploading...': 'Cargando archivos...',\n Floating: 'Flotante',\n 'Full screen': 'Pantalla completa',\n 'Good response': 'Buena respuesta',\n 'Help me create a new simulation scenario': 'Ayúdame a crear un nuevo escenario de simulación',\n 'Hide conversations': 'Ocultar conversaciones',\n 'How can I help you, {name}?': '¿En qué puedo ayudarle, {name}?',\n 'How can {agent} help you, {name}?': '¿En qué puede ayudarle {agent}, {name}?',\n 'How do I configure detection rules?': '¿Cómo configuro las reglas de detección?',\n 'Image could not be loaded': 'No se ha podido cargar la imagen',\n 'Image preview': 'Vista previa de la imagen',\n 'Incorporating your message…': 'Incorporando su mensaje…',\n Input: 'Entrada',\n 'Insert prompt template': 'Insertar plantilla de prompt',\n 'Listening...': 'Escuchando...',\n 'Load earlier messages': 'Cargar mensajes anteriores',\n 'Loading image…': 'Cargando imagen…',\n 'MCP & dynamic tools': 'MCP y herramientas dinámicas',\n 'Mention an agent': 'Mencionar un agente',\n 'Model reasoning': 'Razonamiento del modelo',\n 'Move to workspace': 'Mover a un espacio de trabajo',\n 'Mute microphone': 'Silenciar el micrófono',\n Muted: 'Silenciado',\n 'New chat': 'Nuevo chat',\n 'New conversation': 'Nueva conversación',\n 'New conversation in this workspace': 'Nueva conversación en este espacio de trabajo',\n 'New conversation — send a message to start it': 'Nueva conversación: envíe un mensaje para iniciarla',\n 'New workspace': 'Nuevo espacio de trabajo',\n No: 'No',\n 'No agent matches': 'No hay ningún agente que coincida',\n 'No conversation matches': 'No hay conversaciones que coincidan',\n 'No conversations yet': 'Aún no hay conversaciones',\n 'No prompt matches': 'No hay ningún prompt que coincida',\n 'No response.': 'Sin respuesta.',\n 'Not in a workspace': 'Sin espacio de trabajo',\n 'Not rendered yet': 'Renderizado no iniciado',\n Open: 'Abrir',\n 'Open conversation': 'Abrir conversación',\n 'Open in XTM One': 'Abrir en XTM One',\n 'Other workspaces': 'Otros espacios de trabajo',\n Output: 'Salida',\n 'Play Space Invaders': 'Jugar a Space Invaders',\n 'Play Space Invaders in XTM One': 'Jugar a Space Invaders en XTM One',\n 'Polishing the answer': 'Puliendo la respuesta',\n PowerPoint: 'PowerPoint',\n 'PowerPoint (editable)': 'PowerPoint (editable)',\n Presentation: 'Presentación',\n 'Putting it together': 'Juntando las piezas',\n Quota: 'Cuota',\n 'Quota reached': 'Cuota alcanzada',\n 'Read aloud': 'Leer en voz alta',\n 'Reasoning details': 'Detalles del razonamiento',\n 'Reasoning details — turn limit reached': 'Detalles del razonamiento: se ha alcanzado el límite de turnos',\n 'Reference a conversation': 'Hacer referencia a una conversación',\n 'Remove from workspace': 'Quitar del espacio de trabajo',\n 'Rename conversation': 'Cambiar el nombre de la conversación',\n 'Rename workspace': 'Cambiar el nombre del espacio de trabajo',\n 'Rendering {progress}%': 'Renderizando {progress} %',\n Report: 'Informe',\n 'Response ready': 'Respuesta lista',\n 'Reticulating splines': 'Reticulando splines',\n Retry: 'Reintentar',\n Save: 'Guardar',\n 'Search agents...': 'Buscar agentes...',\n 'Search conversations...': 'Buscar conversaciones...',\n 'Search prompts...': 'Buscar prompts...',\n Send: 'Enviar',\n 'Send now': 'Enviar ahora',\n 'Send when the answer ends': 'Enviar cuando termine la respuesta',\n 'Sending…': 'Enviando…',\n 'Sends when the answer ends:': 'Se enviará cuando termine la respuesta:',\n 'Shared with you': 'Compartido con usted',\n 'Show conversations': 'Mostrar conversaciones',\n Sidebar: 'Barra lateral',\n Skip: 'Omitir',\n 'Skip response': 'Omitir la respuesta',\n 'Sorry, an error occurred. Please try again.': 'Lo sentimos, se ha producido un error. Inténtelo de nuevo.',\n 'Speak or tap to interrupt': 'Habla o toca para interrumpir',\n 'Speaking…': 'Hablando…',\n 'Stop dictation': 'Detener el dictado',\n 'Stop generating': 'Detener la generación',\n 'Stop reading': 'Detener la lectura',\n Stopped: 'Detenido',\n 'Stopping…': 'Deteniendo…',\n Suggestions: 'Sugerencias',\n 'Summarize my recent findings': 'Resume mis hallazgos recientes',\n 'Summarized conversation': 'Conversación resumida',\n 'Switch to': 'Cambiar a',\n 'Switch to another agent': 'Cambiar a otro agente',\n 'Switch view': 'Cambiar de vista',\n 'System prompt': 'Prompt del sistema',\n 'Tap to cancel': 'Toca para cancelar',\n 'Tell us what went wrong (optional)': 'Indíquenos qué ha fallado (opcional)',\n 'The agent needs your approval to run a tool:': 'El agente necesita su aprobación para ejecutar una herramienta:',\n 'The agent needs your approval to run these tools:': 'El agente necesita su aprobación para ejecutar estas herramientas:',\n 'The conversation could not be moved': 'No se ha podido mover la conversación',\n 'The render did not finish': 'El renderizado no ha finalizado',\n 'The workspace could not be created': 'No se ha podido crear el espacio de trabajo',\n 'The workspace could not be deleted': 'No se ha podido eliminar el espacio de trabajo',\n 'The workspace could not be renamed': 'No se ha podido cambiar el nombre del espacio de trabajo',\n 'Thinking it through': 'Reflexionando sobre ello',\n 'Thinking...': 'Pensando...',\n 'This conversation could not be loaded.': 'No se ha podido cargar esta conversación.',\n 'This decision could not be sent. Reload the chat and try again.': 'No se ha podido enviar esta decisión. Actualice el chat e inténtelo de nuevo.',\n 'This message waits for the current response': 'Este mensaje espera la respuesta actual',\n 'This message will be read with your next one.': 'Este mensaje se leerá con su próximo mensaje.',\n 'This item could not be loaded.': 'No se ha podido cargar este elemento.',\n 'The response could not be stopped. Try again.': 'No se ha podido detener la respuesta. Inténtelo de nuevo.',\n 'This turn is no longer waiting for a decision.': 'Este turno ya no está a la espera de una decisión.',\n 'Tool definitions': 'Definiciones de herramientas',\n 'Tool results': 'Resultados de las herramientas',\n 'Transfer chain': 'Cadena de transferencia',\n 'Transferred from {agent}': 'Transferido desde {agent}',\n 'Transferring to {agent}…': 'Transfiriendo a {agent}…',\n 'Turn limit reached.': 'Se ha alcanzado el límite de turnos.',\n 'Unable to connect. Please check the configuration.': 'No se puede establecer conexión. Compruebe la configuración.',\n 'Unmute microphone': 'Activar el micrófono',\n 'Untitled conversation': 'Conversación sin título',\n Usage: 'Uso',\n 'Usage · {period}': 'Uso · {period}',\n 'Uses AI. Verify results.': 'Utiliza IA. Verifique los resultados.',\n 'Using tools…': 'Usando herramientas…',\n Video: 'Vídeo',\n 'Voice call': 'Llamada de voz',\n 'Waiting before continuing…': 'Esperando antes de continuar…',\n 'Waiting for one background task…': 'Esperando una tarea en segundo plano…',\n 'Waiting for your approval…': 'Esperando su aprobación…',\n 'Waiting for {count} background tasks…': 'Esperando {count} tareas en segundo plano…',\n 'Waiting to render': 'Esperando el renderizado',\n 'What are the latest attack patterns?': '¿Cuáles son los últimos patrones de ataque?',\n 'What could be improved?': '¿Qué se podría mejorar?',\n 'Why not? The agent sees this and can adapt (optional)': '¿Por qué no? El agente ve esto y puede adaptarse (opcional)',\n 'Workspace name': 'Nombre del espacio de trabajo',\n Workspaces: 'Espacios de trabajo',\n 'Wrapping things up': 'Finalizando',\n Yes: 'Sí',\n 'Yes, always': 'Sí, siempre',\n 'Your answer is ready': 'Su respuesta está lista',\n default: 'predeterminado',\n 'e.g. wrong environment — use staging instead': 'Por ejemplo, entorno incorrecto: utilice el entorno de pruebas en su lugar',\n 'just now': 'hace un momento',\n 'the agent': 'el agente',\n 'this month': 'este mes',\n 'this year': 'este año',\n today: 'hoy',\n 'unknown tool': 'herramienta desconocida',\n '{agent} has finished': '{agent} ha finalizado',\n '{counts} tokens': '{counts} tokens',\n '{count} iterations': '{count} iteraciones',\n '{count} scenes': '{count} escenas',\n '{count} sections': '{count} secciones',\n '{count} slides': '{count} diapositivas',\n '{count} tool calls': '{count} llamadas a herramientas',\n '{count} transfers': '{count} transferencias',\n '{count}d ago': 'hace {count} d',\n '{count}h ago': 'hace {count} h',\n '{count}m ago': 'hace {count} min',\n '{decided}/{total} decided': '{decided}/{total} decididos',\n '{percent}% full': '{percent} % completo',\n '{quota} (shared across all users)': '{quota} (compartido entre todos los usuarios)',\n '{summary} — click for details': '{summary} — haga clic para ver más detalles',\n '{tool} (+{count} more)…': '{tool} (+{count} más)…',\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 'Se agotó el presupuesto de iteraciones del agente: la ejecución se detuvo antes de completar todos los pasos previstos. La respuesta final es un resumen aproximado del trabajo realizado hasta ahora.',\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 '«Sí, siempre» guarda una preferencia para ti. Esa herramienta se ejecutará entonces sin preguntar, incluso en ejecuciones programadas que nadie supervisa, hasta que la revoques.',\n};\n\nexport default es;\n","import type { Translate } from '../types';\nimport es from './es';\nimport fr from './fr';\nimport ja from './ja';\n\n/** The panel's own translations of its keys, which are its English text. */\nexport type Catalog = Readonly<Record<string, string>>;\n\n// Languages with a catalog; any other is translated through `t`.\nconst CATALOGS: Readonly<Record<string, Catalog>> = { es, fr, ja };\n\n/** The catalog for *locale* (`fr-FR` reads `fr`), or none for English and languages the panel lacks. */\nexport function catalogFor(locale: string | undefined): Catalog | undefined {\n const tag = locale ?? (typeof navigator !== 'undefined' ? navigator.language : undefined);\n const language = tag?.toLowerCase().split(/[-_]/)[0];\n return language ? CATALOGS[language] : undefined;\n}\n\n/**\n * The panel's translator: the host's `t` where it translates a key (returns\n * something other than the key), the panel's own catalog otherwise. A host\n * that translates but names no `locale` keeps its English, not the browser's language.\n */\nexport function createTranslate(hostT: Translate | undefined, locale: string | undefined): Translate {\n const catalog = locale || !hostT ? catalogFor(locale) : undefined;\n return (key) => {\n const hosted = hostT?.(key);\n if (hosted && hosted !== key) return hosted;\n return catalog?.[key] ?? key;\n };\n}\n","import type { Catalog } from './index';\n\nconst fr: Catalog = {\n '(no output)': '(pas de sortie)',\n '1 tool call': '1 appel d’outil',\n '1 transfer': '1 transfert',\n 'A file failed to upload: remove it, or send once the answer ends':\n 'Un fichier n’a pas pu être envoyé : retirez-le, ou envoyez une fois la réponse terminée',\n 'A message references at most {count} conversations': 'Un message fait référence à {count} conversations au maximum',\n 'Acting on the page…': 'Action sur la page…',\n 'Agent switching is not available here': 'Le changement d’agent n’est pas disponible ici',\n 'Almost there': 'On y est presque',\n 'Also applies to your scheduled runs, until you revoke it': 'S’applique également à vos exécutions planifiées, jusqu’à ce que vous la révoquiez',\n 'Always allowed': 'Toujours autorisé',\n 'Analyzing results…': 'Analyse des résultats…',\n 'Analyzing the details': 'Analyse des détails',\n Approved: 'Approuvé',\n 'Ask Assistant': 'Demander à l’assistant',\n 'Ask a question...': 'Poser une question...',\n Assistant: 'Assistant',\n 'Attach files': 'Joindre des fichiers',\n 'Attached files': 'Fichiers joints',\n 'Attachments wait for the current response': 'Les pièces jointes attendent la réponse en cours',\n Back: 'Retour',\n 'Bad response': 'Réponse incorrecte',\n 'Browse agents': 'Parcourir les agents',\n Cancel: 'Annuler',\n 'Cancel question': 'Annuler la question',\n 'Clear all conversations': 'Effacer toutes les conversations',\n Close: 'Fermer',\n 'Collecting results from one task…': 'Collecte des résultats d’une tâche…',\n 'Collecting results from {count} tasks…': 'Collecte des résultats de {count} tâches…',\n 'Composing answer…': 'Rédaction de la réponse…',\n Confirm: 'Confirmer',\n 'Connecting the dots': 'Relier les points',\n 'Consulting the sources': 'Consulter les sources',\n 'Consulting {agent}…': 'Consultation de {agent}…',\n 'Context full — older turns are being dropped': 'Contexte complet — les tours plus anciens sont supprimés',\n 'Context nearly full — older turns are being summarized': 'Contexte presque plein — les tours plus anciens sont en cours de synthèse',\n 'Context used': 'Contexte utilisé',\n 'Continue now': 'Continuer maintenant',\n Conversation: 'Conversation',\n 'Conversation history': 'Historique des conversations',\n 'Conversation title': 'Titre de la conversation',\n 'Conversation transcript': 'Fil de la conversation',\n Copied: 'Copié',\n 'Copied!': 'Copié !',\n 'Copy code': 'Copier le code',\n 'Copy message': 'Copier le message',\n 'Copy response': 'Copier la réponse',\n 'Could not reach the assistant service. Check the connection and try again.':\n 'Impossible d’accéder au service d’assistant. Vérifiez la connexion et réessayez.',\n 'Could not send your decision. Please try again.': 'Impossible d’envoyer votre décision. Veuillez réessayer.',\n 'Create agent': 'Créer un agent',\n 'Crunching the data': 'Traitement des données',\n 'Decline this call': 'Refuser cet appel',\n Declined: 'Refusé',\n 'Delegating one task…': 'Délégation d’une tâche…',\n 'Delegating {count} tasks…': 'Délégation de {count} tâches…',\n 'Delete all conversations': 'Supprimer toutes les conversations',\n 'Delete all conversations?': 'Supprimer toutes les conversations ?',\n 'Delete conversation': 'Supprimer la conversation',\n 'Delete workspace': 'Supprimer l’espace de travail',\n 'Delete?': 'Supprimer ?',\n 'Dictate a message': 'Dicter un message',\n 'Did not answer': 'N’a pas répondu',\n Download: 'Télécharger',\n 'Download {format}': 'Télécharger en {format}',\n 'Drop a conversation here, or start one with +.': 'Déposez une conversation ici ou démarrez-en une avec +.',\n 'Drop files to attach them': 'Déposez des fichiers pour les joindre',\n 'End voice call': 'Terminer l’appel vocal',\n 'Enter to send now · Esc to stop': 'Appuyez sur Entrée pour envoyer maintenant · Appuyez sur Échap pour arrêter',\n 'Enter to send when the answer ends · Esc to stop': 'Appuyez sur Entrée pour envoyer à la fin de la réponse · Appuyez sur Échap pour arrêter',\n 'Every conversation is in a workspace.': 'Toutes les conversations sont dans un espace de travail.',\n 'Expand image': 'Agrandir l’image',\n 'Failed to get a response. Resuming…': 'Aucune réponse reçue. Reprise…',\n 'Feedback already submitted': 'Avis déjà envoyé',\n 'Files uploading...': 'Envoi des fichiers en cours...',\n Floating: 'Flottant',\n 'Full screen': 'Plein écran',\n 'Good response': 'Bonne réponse',\n 'Help me create a new simulation scenario': 'Aidez-moi à créer un nouveau scénario de simulation',\n 'Hide conversations': 'Masquer les conversations',\n 'How can I help you, {name}?': 'Comment puis-je vous aider, {name} ?',\n 'How can {agent} help you, {name}?': 'Comment {agent} peut-il vous aider, {name} ?',\n 'How do I configure detection rules?': 'Comment configurer les règles de détection ?',\n 'Image could not be loaded': 'Impossible de charger l’image',\n 'Image preview': 'Aperçu de l’image',\n 'Incorporating your message…': 'Intégration de votre message…',\n Input: 'Entrée',\n 'Insert prompt template': 'Insérer un modèle de prompt',\n 'Listening...': 'Écoute en cours...',\n 'Load earlier messages': 'Charger les messages précédents',\n 'Loading image…': 'Chargement de l’image…',\n 'MCP & dynamic tools': 'MCP et outils dynamiques',\n 'Mention an agent': 'Mentionner un agent',\n 'Model reasoning': 'Raisonnement du modèle',\n 'Move to workspace': 'Déplacer vers un espace de travail',\n 'Mute microphone': 'Couper le micro',\n Muted: 'Micro coupé',\n 'New chat': 'Nouveau chat',\n 'New conversation': 'Nouvelle conversation',\n 'New conversation in this workspace': 'Nouvelle conversation dans cet espace de travail',\n 'New conversation — send a message to start it': 'Nouvelle conversation — envoyez un message pour la démarrer',\n 'New workspace': 'Nouvel espace de travail',\n No: 'Non',\n 'No agent matches': 'Aucun agent ne correspond',\n 'No conversation matches': 'Aucune conversation ne correspond',\n 'No conversations yet': 'Aucune conversation pour l’instant',\n 'No prompt matches': 'Aucun prompt ne correspond',\n 'No response.': 'Pas de réponse.',\n 'Not in a workspace': 'Hors espace de travail',\n 'Not rendered yet': 'Rendu non lancé',\n Open: 'Ouvrir',\n 'Open conversation': 'Ouvrir la conversation',\n 'Open in XTM One': 'Ouvrir dans XTM One',\n 'Other workspaces': 'Autres espaces de travail',\n Output: 'Sortie',\n 'Play Space Invaders': 'Jouer à Space Invaders',\n 'Play Space Invaders in XTM One': 'Jouer à Space Invaders dans XTM One',\n 'Polishing the answer': 'Peaufiner la réponse',\n PowerPoint: 'PowerPoint',\n 'PowerPoint (editable)': 'PowerPoint (modifiable)',\n Presentation: 'Présentation',\n 'Putting it together': 'Assemblage des éléments',\n Quota: 'Quota',\n 'Quota reached': 'Quota atteint',\n 'Read aloud': 'Lire à voix haute',\n 'Reasoning details': 'Détails du raisonnement',\n 'Reasoning details — turn limit reached': 'Détails du raisonnement — limite de tours atteinte',\n 'Reference a conversation': 'Faire référence à une conversation',\n 'Remove from workspace': 'Retirer de l’espace de travail',\n 'Rename conversation': 'Renommer la conversation',\n 'Rename workspace': 'Renommer l’espace de travail',\n 'Rendering {progress}%': 'Rendu en cours : {progress} %',\n Report: 'Rapport',\n 'Response ready': 'Réponse prête',\n 'Reticulating splines': 'Réticulation des splines',\n Retry: 'Réessayer',\n Save: 'Sauvegarder',\n 'Search agents...': 'Rechercher des agents...',\n 'Search conversations...': 'Rechercher des conversations...',\n 'Search prompts...': 'Rechercher des prompts...',\n Send: 'Envoyer',\n 'Send now': 'Envoyer maintenant',\n 'Send when the answer ends': 'Envoyer à la fin de la réponse',\n 'Sending…': 'Envoi en cours…',\n 'Sends when the answer ends:': 'Envoi à la fin de la réponse :',\n 'Shared with you': 'Partagé avec vous',\n 'Show conversations': 'Afficher les conversations',\n Sidebar: 'Barre latérale',\n Skip: 'Passer',\n 'Skip response': 'Passer la réponse',\n 'Sorry, an error occurred. Please try again.': 'Désolé, une erreur s’est produite. Veuillez réessayer.',\n 'Speak or tap to interrupt': 'Parlez ou touchez pour interrompre',\n 'Speaking…': 'Réponse en cours…',\n 'Stop dictation': 'Arrêter la dictée',\n 'Stop generating': 'Arrêter la génération',\n 'Stop reading': 'Arrêter la lecture',\n Stopped: 'Arrêté',\n 'Stopping…': 'Arrêt en cours…',\n Suggestions: 'Suggestions',\n 'Summarize my recent findings': 'Résumez mes dernières découvertes',\n 'Summarized conversation': 'Résumé de la conversation',\n 'Switch to': 'Basculer vers',\n 'Switch to another agent': 'Passer à un autre agent',\n 'Switch view': 'Changer de vue',\n 'System prompt': 'Prompt système',\n 'Tap to cancel': 'Touchez pour annuler',\n 'Tell us what went wrong (optional)': 'Dites-nous ce qui n’a pas fonctionné (facultatif)',\n 'The agent needs your approval to run a tool:': 'L’agent a besoin de votre accord pour exécuter un outil :',\n 'The agent needs your approval to run these tools:': 'L’agent a besoin de votre accord pour exécuter ces outils :',\n 'The conversation could not be moved': 'Impossible de déplacer la conversation',\n 'The render did not finish': 'Le rendu n’a pas abouti',\n 'The workspace could not be created': 'Impossible de créer l’espace de travail',\n 'The workspace could not be deleted': 'Impossible de supprimer l’espace de travail',\n 'The workspace could not be renamed': 'Impossible de renommer l’espace de travail',\n 'Thinking it through': 'Réfléchir à la question',\n 'Thinking...': 'Je réfléchis...',\n 'This conversation could not be loaded.': 'Impossible de charger cette conversation.',\n 'This decision could not be sent. Reload the chat and try again.': 'Cette décision n’a pas pu être envoyée. Actualisez le chat et réessayez.',\n 'This message waits for the current response': 'Ce message attend la réponse en cours',\n 'This message will be read with your next one.': 'Ce message sera lu avec votre prochain message.',\n 'This item could not be loaded.': 'Impossible de charger cet élément.',\n 'The response could not be stopped. Try again.': 'La réponse n’a pas pu être arrêtée. Réessayez.',\n 'This turn is no longer waiting for a decision.': 'Ce tour n’attend plus de décision.',\n 'Tool definitions': 'Définitions des outils',\n 'Tool results': 'Résultats des outils',\n 'Transfer chain': 'Chaîne de transfert',\n 'Transferred from {agent}': 'Transféré depuis {agent}',\n 'Transferring to {agent}…': 'Transfert vers {agent}…',\n 'Turn limit reached.': 'Limite de tours atteinte.',\n 'Unable to connect. Please check the configuration.': 'Impossible de se connecter. Veuillez vérifier la configuration.',\n 'Unmute microphone': 'Réactiver le micro',\n 'Untitled conversation': 'Conversation sans titre',\n Usage: 'Utilisation',\n 'Usage · {period}': 'Utilisation · {period}',\n 'Uses AI. Verify results.': 'Utilise l’IA. Vérifiez les résultats.',\n 'Using tools…': 'Utilisation des outils…',\n Video: 'Vidéo',\n 'Voice call': 'Appel vocal',\n 'Waiting before continuing…': 'En attente avant de continuer…',\n 'Waiting for one background task…': 'En attente d’une tâche en arrière-plan…',\n 'Waiting for your approval…': 'En attente de votre accord…',\n 'Waiting for {count} background tasks…': 'En attente de {count} tâches en arrière-plan…',\n 'Waiting to render': 'En attente du rendu',\n 'What are the latest attack patterns?': 'Quels sont les derniers schémas d’attaque ?',\n 'What could be improved?': 'Qu’est-ce qui pourrait être amélioré ?',\n 'Why not? The agent sees this and can adapt (optional)': 'Pourquoi pas ? L’agent en prend connaissance et peut s’adapter (facultatif)',\n 'Workspace name': 'Nom de l’espace de travail',\n Workspaces: 'Espaces de travail',\n 'Wrapping things up': 'Pour conclure',\n Yes: 'Oui',\n 'Yes, always': 'Oui, toujours',\n 'Your answer is ready': 'Votre réponse est prête',\n default: 'par défaut',\n 'e.g. wrong environment — use staging instead': 'Par exemple : environnement incorrect — utilisez plutôt l’environnement de préproduction',\n 'just now': 'à l’instant',\n 'the agent': 'l’agent',\n 'this month': 'ce mois-ci',\n 'this year': 'cette année',\n today: 'aujourd’hui',\n 'unknown tool': 'outil inconnu',\n '{agent} has finished': '{agent} a terminé',\n '{counts} tokens': '{counts} tokens',\n '{count} iterations': '{count} itérations',\n '{count} scenes': '{count} scènes',\n '{count} sections': '{count} sections',\n '{count} slides': '{count} diapositives',\n '{count} tool calls': '{count} appels d’outils',\n '{count} transfers': '{count} transferts',\n '{count}d ago': 'il y a {count} j',\n '{count}h ago': 'il y a {count} h',\n '{count}m ago': 'il y a {count} min',\n '{decided}/{total} decided': '{decided}/{total} décidés',\n '{percent}% full': '{percent} % rempli',\n '{quota} (shared across all users)': '{quota} (partagé entre tous les utilisateurs)',\n '{summary} — click for details': '{summary} — cliquez pour plus de détails',\n '{tool} (+{count} more)…': '{tool} (+{count} autres)…',\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 'Le budget d’itérations de l’agent est épuisé : l’exécution s’est arrêtée avant la fin de toutes les étapes prévues. La réponse finale résume au mieux le travail accompli jusqu’ici.',\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 '« Oui, toujours » enregistre une préférence pour vous. Cet outil s’exécutera alors sans demander, y compris lors des exécutions planifiées que personne ne surveille, jusqu’à ce que vous la révoquiez.',\n};\n\nexport default fr;\n","import type { Catalog } from './index';\n\nconst ja: Catalog = {\n '(no output)': '(出力なし)',\n '1 tool call': 'ツール呼び出し 1 件',\n '1 transfer': '転送 1 件',\n 'A file failed to upload: remove it, or send once the answer ends':\n 'ファイルのアップロードに失敗しました。削除するか、応答の終了後に送信してください',\n 'A message references at most {count} conversations': '1 つのメッセージで参照できる会話は最大 {count} 件です',\n 'Acting on the page…': 'ページを操作中…',\n 'Agent switching is not available here': 'ここではエージェントの切り替えは利用できません',\n 'Almost there': 'もうすぐ完了します',\n 'Also applies to your scheduled runs, until you revoke it': '予定された実行についても、取り消すまでは同様に適用されます',\n 'Always allowed': '常に許可',\n 'Analyzing results…': '結果を分析中…',\n 'Analyzing the details': '詳細を分析しています',\n Approved: '承認済み',\n 'Ask Assistant': 'アシスタントに質問',\n 'Ask a question...': '質問する...',\n Assistant: 'アシスタント',\n 'Attach files': 'ファイルを添付',\n 'Attached files': '添付ファイル',\n 'Attachments wait for the current response': '添付ファイルは、現在の応答を待機します',\n Back: '戻る',\n 'Bad response': '不適切な回答',\n 'Browse agents': 'エージェントを閲覧する',\n Cancel: 'キャンセル',\n 'Cancel question': '質問をキャンセル',\n 'Clear all conversations': 'すべての会話を消去',\n Close: '閉じる',\n 'Collecting results from one task…': '1 件のタスクの結果を収集中…',\n 'Collecting results from {count} tasks…': '{count} 件のタスクの結果を収集中…',\n 'Composing answer…': '回答を作成中…',\n Confirm: '確認する',\n 'Connecting the dots': '情報を関連付けています',\n 'Consulting the sources': '情報源を確認しています',\n 'Consulting {agent}…': '{agent} に問い合わせ中…',\n 'Context full — older turns are being dropped': 'コンテキストが満杯 — 古いターンが削除されています',\n 'Context nearly full — older turns are being summarized': 'コンテキストがほぼ満杯です — 古いターンが要約されています',\n 'Context used': '使用されたコンテキスト',\n 'Continue now': '今すぐ続行',\n Conversation: '会話',\n 'Conversation history': '会話履歴',\n 'Conversation title': '会話のタイトル',\n 'Conversation transcript': '会話の履歴',\n Copied: 'コピーしました',\n 'Copied!': 'コピー完了!',\n 'Copy code': 'コードをコピー',\n 'Copy message': 'メッセージをコピー',\n 'Copy response': '応答をコピー',\n 'Could not reach the assistant service. Check the connection and try again.':\n 'アシスタントサービスに接続できませんでした。接続状態を確認して、もう一度お試しください。',\n 'Could not send your decision. Please try again.': '決定内容を送信できませんでした。もう一度お試しください。',\n 'Create agent': 'エージェントを作成する',\n 'Crunching the data': 'データを処理しています',\n 'Decline this call': 'この呼び出しを拒否',\n Declined: '拒否済み',\n 'Delegating one task…': '1 件のタスクを委任中…',\n 'Delegating {count} tasks…': '{count} 件のタスクを委任中…',\n 'Delete all conversations': 'すべての会話を削除',\n 'Delete all conversations?': 'すべての会話を削除しますか?',\n 'Delete conversation': '会話を削除',\n 'Delete workspace': 'ワークスペースを削除',\n 'Delete?': '削除しますか?',\n 'Dictate a message': 'メッセージを音声入力する',\n 'Did not answer': '回答なし',\n Download: 'ダウンロード',\n 'Download {format}': '{format} をダウンロード',\n 'Drop a conversation here, or start one with +.': 'ここに会話をドロップするか、+ で新しく開始してください。',\n 'Drop files to attach them': 'ファイルをドロップして添付',\n 'End voice call': '音声通話を終了',\n 'Enter to send now · Esc to stop': 'Enterキーで今すぐ送信 · Escキーで停止',\n 'Enter to send when the answer ends · Esc to stop': 'Enterキーで回答の終了後に送信 · Escキーで停止',\n 'Every conversation is in a workspace.': 'すべての会話がワークスペースに含まれています。',\n 'Expand image': '画像を拡大',\n 'Failed to get a response. Resuming…': '応答を取得できませんでした。再開しています…',\n 'Feedback already submitted': 'フィードバックは送信済みです',\n 'Files uploading...': 'ファイルのアップロード中...',\n Floating: 'フローティング',\n 'Full screen': '全画面表示',\n 'Good response': '適切な回答',\n 'Help me create a new simulation scenario': '新しいシミュレーションシナリオの作成を手伝ってください',\n 'Hide conversations': '会話を非表示にする',\n 'How can I help you, {name}?': '{name} さん、何かお手伝いできることはありますか?',\n 'How can {agent} help you, {name}?': '{name} さん、{agent} がお手伝いできることはありますか?',\n 'How do I configure detection rules?': '検出ルールはどのように設定すればよいですか?',\n 'Image could not be loaded': '画像を読み込めませんでした',\n 'Image preview': '画像プレビュー',\n 'Incorporating your message…': 'メッセージを組み込んでいます…',\n Input: '入力',\n 'Insert prompt template': 'プロンプトテンプレートを挿入',\n 'Listening...': '聞き取り中...',\n 'Load earlier messages': '以前のメッセージを読み込む',\n 'Loading image…': '画像を読み込み中…',\n 'MCP & dynamic tools': 'MCPと動的ツール',\n 'Mention an agent': 'エージェントをメンション',\n 'Model reasoning': 'モデルの推論',\n 'Move to workspace': 'ワークスペースに移動',\n 'Mute microphone': 'マイクをミュート',\n Muted: 'ミュート中',\n 'New chat': '新しいチャット',\n 'New conversation': '新しい会話',\n 'New conversation in this workspace': 'このワークスペースで新しい会話',\n 'New conversation — send a message to start it': '新しい会話 — メッセージを送信すると開始されます',\n 'New workspace': '新しいワークスペース',\n No: 'いいえ',\n 'No agent matches': '該当するエージェントはありません',\n 'No conversation matches': '一致する会話はありません',\n 'No conversations yet': 'まだ会話はありません',\n 'No prompt matches': '一致するプロンプトはありません',\n 'No response.': '応答なし。',\n 'Not in a workspace': 'ワークスペースなし',\n 'Not rendered yet': 'まだレンダリングされていません',\n Open: '開く',\n 'Open conversation': '会話を開く',\n 'Open in XTM One': 'XTM One で開く',\n 'Other workspaces': 'その他のワークスペース',\n Output: '出力',\n 'Play Space Invaders': 'Space Invaders をプレイ',\n 'Play Space Invaders in XTM One': 'XTM One で Space Invaders をプレイ',\n 'Polishing the answer': '回答を仕上げています',\n PowerPoint: 'PowerPoint',\n 'PowerPoint (editable)': 'PowerPoint(編集可能)',\n Presentation: 'プレゼンテーション',\n 'Putting it together': '内容をまとめています',\n Quota: 'クォータ',\n 'Quota reached': 'クォータに達しました',\n 'Read aloud': '読み上げ',\n 'Reasoning details': '推論の詳細',\n 'Reasoning details — turn limit reached': '推論の詳細 — ターン制限に達しました',\n 'Reference a conversation': '会話を参照',\n 'Remove from workspace': 'ワークスペースから外す',\n 'Rename conversation': '会話の名前を変更',\n 'Rename workspace': 'ワークスペースの名前を変更',\n 'Rendering {progress}%': 'レンダリング中 {progress}%',\n Report: 'レポート',\n 'Response ready': '応答準備完了',\n 'Reticulating splines': 'スプラインを網状化しています',\n Retry: '再試行',\n Save: '保存',\n 'Search agents...': 'エージェントを検索...',\n 'Search conversations...': '会話を検索...',\n 'Search prompts...': 'プロンプトを検索...',\n Send: '送信',\n 'Send now': '今すぐ送信',\n 'Send when the answer ends': '回答の終了後に送信',\n 'Sending…': '送信中…',\n 'Sends when the answer ends:': '回答の終了後に送信:',\n 'Shared with you': '他のユーザーから共有',\n 'Show conversations': '会話を表示',\n Sidebar: 'サイドバー',\n Skip: 'スキップ',\n 'Skip response': '応答をスキップ',\n 'Sorry, an error occurred. Please try again.': '申し訳ありません。エラーが発生しました。もう一度お試しください。',\n 'Speak or tap to interrupt': '話すかタップして中断',\n 'Speaking…': '読み上げ中…',\n 'Stop dictation': 'ディクテーションを停止',\n 'Stop generating': '生成を停止',\n 'Stop reading': '読み上げを停止',\n Stopped: '停止',\n 'Stopping…': '停止中…',\n Suggestions: '提案',\n 'Summarize my recent findings': '私の最近の発見を要約してください',\n 'Summarized conversation': '会話の要約',\n 'Switch to': '切り替え先',\n 'Switch to another agent': '別のエージェントに切り替える',\n 'Switch view': 'ビューを切り替える',\n 'System prompt': 'システムプロンプト',\n 'Tap to cancel': 'タップしてキャンセル',\n 'Tell us what went wrong (optional)': '問題があった点をお聞かせください(任意)',\n 'The agent needs your approval to run a tool:': 'エージェントがツールを実行するには、あなたの承認が必要です:',\n 'The agent needs your approval to run these tools:': 'エージェントがこれらのツールを実行するには、あなたの承認が必要です:',\n 'The conversation could not be moved': '会話を移動できませんでした',\n 'The render did not finish': 'レンダリングが完了しませんでした',\n 'The workspace could not be created': 'ワークスペースを作成できませんでした',\n 'The workspace could not be deleted': 'ワークスペースを削除できませんでした',\n 'The workspace could not be renamed': 'ワークスペースの名前を変更できませんでした',\n 'Thinking it through': 'じっくり考えています',\n 'Thinking...': '考え中...',\n 'This conversation could not be loaded.': 'この会話を読み込めませんでした。',\n 'This decision could not be sent. Reload the chat and try again.':\n 'この決定を送信できませんでした。チャットを再読み込みして、もう一度お試しください。',\n 'This message waits for the current response': 'このメッセージは、現在の応答を待機します',\n 'This message will be read with your next one.': 'このメッセージは次のメッセージと一緒に読まれます。',\n 'This item could not be loaded.': 'この項目を読み込めませんでした。',\n 'The response could not be stopped. Try again.': '応答を停止できませんでした。もう一度お試しください。',\n 'This turn is no longer waiting for a decision.': 'このターンでは、決定を待つ必要がなくなりました。',\n 'Tool definitions': 'ツールの定義',\n 'Tool results': 'ツールの結果',\n 'Transfer chain': '転送チェーン',\n 'Transferred from {agent}': '{agent} から転送されました',\n 'Transferring to {agent}…': '{agent} に転送中…',\n 'Turn limit reached.': 'ターン制限に達しました。',\n 'Unable to connect. Please check the configuration.': '接続できません。設定を確認してください。',\n 'Unmute microphone': 'マイクのミュートを解除',\n 'Untitled conversation': 'タイトルのない会話',\n Usage: '使用状況',\n 'Usage · {period}': '使用状況 · {period}',\n 'Uses AI. Verify results.': 'AIを使用しています。結果を確認してください。',\n 'Using tools…': 'ツールを使用中…',\n Video: '動画',\n 'Voice call': '音声通話',\n 'Waiting before continuing…': '続行する前に待機中…',\n 'Waiting for one background task…': 'バックグラウンドタスク 1 件を待機中…',\n 'Waiting for your approval…': '承認をお待ちしています…',\n 'Waiting for {count} background tasks…': 'バックグラウンドタスク {count} 件を待機中…',\n 'Waiting to render': 'レンダリング待ち',\n 'What are the latest attack patterns?': '最新の攻撃パターンにはどのようなものがありますか?',\n 'What could be improved?': '改善できる点はありますか?',\n 'Why not? The agent sees this and can adapt (optional)': 'なぜですか? エージェントはこれを確認し、対応を調整できます(任意)',\n 'Workspace name': 'ワークスペース名',\n Workspaces: 'ワークスペース',\n 'Wrapping things up': '仕上げをしています',\n Yes: 'はい',\n 'Yes, always': 'はい、常に',\n 'Your answer is ready': '回答の準備が整いました',\n default: 'デフォルト',\n 'e.g. wrong environment — use staging instead': '例:環境が間違っている — 代わりにステージング環境を使用してください',\n 'just now': 'たった今',\n 'the agent': 'エージェント',\n 'this month': '今月',\n 'this year': '今年',\n today: '今日',\n 'unknown tool': '不明なツール',\n '{agent} has finished': '{agent} が完了しました',\n '{counts} tokens': '{counts} トークン',\n '{count} iterations': '{count} 回の反復',\n '{count} scenes': '{count} シーン',\n '{count} sections': '{count} セクション',\n '{count} slides': '{count} スライド',\n '{count} tool calls': 'ツール呼び出し {count} 件',\n '{count} transfers': '転送 {count} 件',\n '{count}d ago': '{count} 日前',\n '{count}h ago': '{count} 時間前',\n '{count}m ago': '{count} 分前',\n '{decided}/{total} decided': '{decided}/{total} 件が決定済み',\n '{percent}% full': '{percent} % 使用中',\n '{quota} (shared across all users)': '{quota}(全ユーザーで共有)',\n '{summary} — click for details': '{summary} — クリックで詳細を表示',\n '{tool} (+{count} more)…': '{tool}(他 {count} 件)…',\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 '“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};\n\nexport default ja;\n","import type { ApiEndpoints, BackendType, ChatMessagePersistedFeedback, MessageFeedback } from '../types';\nimport { optInRoute } from './routes';\n\n/**\n * Persisted feedback on an assistant answer: the requests the thumbs send.\n * A stored rating is read back by `parsePersistedFeedback` with the rest of a\n * session restore (`hooks/protocols/parseRestEvent.ts`).\n */\n\n/** Longest comment the feedback endpoint stores. */\nexport const FEEDBACK_COMMENT_MAX_LENGTH = 2000;\n\nexport type FeedbackRating = ChatMessagePersistedFeedback['rating'];\n\nexport const ratingOf = (value: MessageFeedback): FeedbackRating => (value === 'up' ? 'positive' : 'negative');\n\nexport const feedbackValueOf = (rating: FeedbackRating): MessageFeedback => (rating === 'positive' ? 'up' : 'down');\n\n/**\n * How the thumbs of one answer work, or null when it shows none:\n * - `persisted`: the panel stores the rating (`ApiEndpoints.feedback`), which\n * needs the conversation and the answer's persisted id - an answer the\n * backend did not identify cannot be rated;\n * - `callback`: the host stores it (`onMessageFeedback` alone).\n */\nexport type FeedbackMode = 'persisted' | 'callback';\n\nexport function feedbackModeOf(\n serverId: string | undefined,\n feedbackUrl: string | null | undefined,\n conversationId: string | null | undefined,\n hasCallback: boolean,\n): FeedbackMode | null {\n if (feedbackUrl) return conversationId && serverId ? 'persisted' : null;\n return hasCallback ? 'callback' : null;\n}\n\n/** The rating shown on an answer, and whether it can still be changed. */\nexport function shownFeedback(\n session: { value: MessageFeedback | null } | undefined,\n stored: ChatMessagePersistedFeedback | undefined,\n): { value: MessageFeedback | null; locked: boolean } {\n // Rated in this session: the session's answer wins, and can be changed.\n if (session) return { value: session.value, locked: false };\n // Rated before the conversation was reopened: shown, and final.\n if (stored) return { value: feedbackValueOf(stored.rating), locked: true };\n return { value: null, locked: false };\n}\n\n/** A comment as the endpoint stores it: trimmed, capped, and null when empty. */\nexport function normalizeFeedbackComment(comment: string | null | undefined): string | null {\n const trimmed = (comment ?? '').trim();\n return trimmed ? trimmed.slice(0, FEEDBACK_COMMENT_MAX_LENGTH) : null;\n}\n\n/**\n * `{apiBaseUrl}{feedback}`, or null when the panel does not store feedback\n * itself: the path is unset (no default, see `ApiEndpoints.feedback`), the\n * backend is not the REST one, or every request goes to one endpoint.\n */\nexport function feedbackBaseUrl(apiBaseUrl: string, apiEndpoints: ApiEndpoints | undefined, backendType: BackendType): string | null {\n return optInRoute(apiBaseUrl, apiEndpoints, backendType, 'feedback');\n}\n\n/**\n * The request that stores (`rating`) or retracts (`null`) the user's rating\n * of one message.\n */\nexport function feedbackRequest(\n baseUrl: string,\n conversationId: string,\n messageId: string,\n rating: { rating: FeedbackRating; comment: string | null } | null,\n requestHeaders?: Record<string, string>,\n): { url: string; init: RequestInit } {\n const url = `${baseUrl}/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}/feedback`;\n if (!rating) {\n return { url, init: { method: 'DELETE', credentials: 'include', headers: { ...requestHeaders } } };\n }\n return {\n url,\n init: {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json', ...requestHeaders },\n body: JSON.stringify({ rating: rating.rating, comment: normalizeFeedbackComment(rating.comment) }),\n },\n };\n}\n\n/** The part of `fetch` the feedback request needs. */\nexport type FeedbackFetch = (url: string, init: RequestInit) => Promise<{ ok: boolean }>;\n\n/**\n * Sends one `feedbackRequest` and says whether the backend stored it. A network\n * failure is a refusal like any other status: the caller rolls the thumbs back\n * either way.\n */\nexport async function submitFeedback(\n fetchImpl: FeedbackFetch,\n baseUrl: string,\n conversationId: string,\n messageId: string,\n rating: { rating: FeedbackRating; comment: string | null } | null,\n requestHeaders?: Record<string, string>,\n): Promise<boolean> {\n const { url, init } = feedbackRequest(baseUrl, conversationId, messageId, rating, requestHeaders);\n try {\n return (await fetchImpl(url, init)).ok;\n } catch {\n return false;\n }\n}\n","import type { ApiEndpoints, BackendType, ChatConversationRef, ChatConversationReferenceCandidate } from '../types';\nimport { optInRoute } from './routes';\n\n/**\n * The composer's `@` grammar, as in the XTM One web chat: only a pick from the\n * menu makes `@key` mean a conversation. No regex lookbehind: the bundle ships\n * untranspiled.\n */\n\n/** What one message references at most - the backend's own bound. */\nexport const MAX_REFERENCED_CONVERSATIONS = 5;\n\n/** How many conversations the menu lists. */\nexport const REFERENCE_MENU_LIMIT = 8;\n\n/** How long the typed text rests before the conversations are searched. */\nexport const REFERENCE_SEARCH_DEBOUNCE_MS = 150;\n\n/** A character a key is made of: what `[\\w-]` matches. */\nconst isKeyChar = (ch: string | undefined): boolean => ch !== undefined && /[\\w-]/.test(ch);\n\n/** A key the grammar reads back as one token. */\nconst KEY_RE = /^[\\w-]+$/;\n\n/** The `@` being typed: where it sits, and what follows it up to the caret. */\nexport interface ReferenceTrigger {\n /** Index of the `@` in the text. */\n anchor: number;\n query: string;\n}\n\n/**\n * The `@` token being typed before the caret, or null.\n *\n * The `@` must not follow a key character, so an email (`user@host`) never\n * opens the menu - the same rule as `referenceTokens`, so whatever opens the\n * menu also parses as a reference once inserted.\n */\nexport function referenceTrigger(textBeforeCaret: string): ReferenceTrigger | null {\n // Only the last `@` can match: nothing between it and the end may be anything\n // but key characters.\n const match = /@([\\w-]*)$/.exec(textBeforeCaret);\n if (!match || isKeyChar(textBeforeCaret[match.index - 1])) return null;\n return { anchor: match.index, query: match[1] };\n}\n\n/** The `@tokens` of *text*, lowercased, each once, in order of appearance. */\nexport function referenceTokens(text: string): string[] {\n const tokens: string[] = [];\n const re = /@([\\w-]+)/g;\n let match = re.exec(text);\n while (match !== null) {\n const key = match[1].toLowerCase();\n if (!isKeyChar(text[match.index - 1]) && !tokens.includes(key)) tokens.push(key);\n match = re.exec(text);\n }\n return tokens;\n}\n\n/**\n * *refs* without the picks whose `@key` *text* no longer holds - the same\n * array when nothing was dropped, so a state setter can bail out.\n */\nexport function pruneConversationRefs(refs: ChatConversationRef[], text: string): ChatConversationRef[] {\n if (refs.length === 0) return refs;\n const tokens = new Set(referenceTokens(text));\n const kept = refs.filter((r) => tokens.has(r.key.toLowerCase()));\n return kept.length === refs.length ? refs : kept;\n}\n\n/** *refs* plus *pick*, unless that conversation is already there. */\nexport function withConversationRef(refs: ChatConversationRef[], pick: ChatConversationRef): ChatConversationRef[] {\n return refs.some((r) => r.conversationId === pick.conversationId) ? refs : [...refs, pick];\n}\n\n/**\n * The conversations a message references: the picks whose `@key` it holds, in\n * the order the keys appear, each once, at most `MAX_REFERENCED_CONVERSATIONS`.\n */\nexport function referencedConversations(refs: ChatConversationRef[], text: string): ChatConversationRef[] {\n if (refs.length === 0) return [];\n const out: ChatConversationRef[] = [];\n for (const token of referenceTokens(text)) {\n for (const ref of refs) {\n if (ref.key.toLowerCase() !== token || out.some((r) => r.conversationId === ref.conversationId)) continue;\n out.push(ref);\n }\n }\n return out.slice(0, MAX_REFERENCED_CONVERSATIONS);\n}\n\n/**\n * *text* with the `@partial` of *trigger* replaced by `@<key> `, and where the\n * caret goes: after the space. A space already following it is reused rather\n * than doubled.\n */\nexport function insertConversationReference(text: string, trigger: ReferenceTrigger, key: string): { text: string; caret: number } {\n const before = text.slice(0, trigger.anchor);\n const after = text.slice(trigger.anchor + 1 + trigger.query.length);\n const token = `@${key}`;\n const space = /^\\s/.test(after) ? '' : ' ';\n return { text: `${before}${token}${space}${after}`, caret: before.length + token.length + 1 };\n}\n\n/**\n * Defensive parse of the menu's candidates (`{ conversations: [...] }` or a\n * bare array). An entry without an id, or whose key would not read back as\n * one `@token`, is skipped: inserting it would reference nothing.\n */\nexport function parseReferenceCandidates(data: unknown): ChatConversationReferenceCandidate[] {\n const list = Array.isArray(data)\n ? data\n : Array.isArray((data as Record<string, unknown> | null)?.conversations)\n ? ((data as Record<string, unknown>).conversations as unknown[])\n : [];\n const out: ChatConversationReferenceCandidate[] = [];\n for (const raw of list) {\n if (!raw || typeof raw !== 'object') continue;\n const c = raw as Record<string, unknown>;\n if (typeof c.id !== 'string' || !c.id || typeof c.key !== 'string' || !KEY_RE.test(c.key)) continue;\n if (out.some((o) => o.id === c.id)) continue;\n out.push({\n id: c.id,\n title: typeof c.title === 'string' ? c.title.trim() : '',\n key: c.key,\n updatedAt: typeof c.updated_at === 'string' ? c.updated_at : undefined,\n // Absent reads as the user's own: the hint is for a share, and a\n // backend that does not say has nothing to warn about.\n isOwn: c.is_own !== false,\n });\n }\n return out;\n}\n\n/**\n * The search URL of the menu, or null while the feature is off: the host did\n * not name the route, or the backend has no per-path routing.\n */\nexport function conversationReferencesUrl(apiBaseUrl: string, apiEndpoints: ApiEndpoints | undefined, backendType: BackendType): string | null {\n return optInRoute(apiBaseUrl, apiEndpoints, backendType, 'conversationReferences');\n}\n\n/** *url* with the menu's query: the typed text, the page size and the conversation written in. */\nexport function referenceSearchUrl(url: string, query: string, exclude?: string | null): string {\n const params = new URLSearchParams({ limit: String(REFERENCE_MENU_LIMIT) });\n if (query) params.set('q', query);\n if (exclude) params.set('exclude', exclude);\n return `${url}${url.includes('?') ? '&' : '?'}${params.toString()}`;\n}\n\n/** What the search last answered, and for which typed text. */\nexport interface ReferenceSearchState {\n query: string | null;\n items: ChatConversationReferenceCandidate[];\n /** The request failed: nothing is known, so nothing is shown. */\n failed: boolean;\n}\n\nexport const EMPTY_REFERENCE_SEARCH: ReferenceSearchState = { query: null, items: [], failed: false };\n\n/**\n * Whether *candidate* answers the typed *query*, by its key or by its title.\n * A title's spaces become hyphens in its key (`Hi Bob` is `@hi-bob`), so the\n * key typed back must also find it by the words of its title.\n */\nexport function referenceMatches(query: string, candidate: { key: string; title: string }): boolean {\n const q = query.toLowerCase();\n if (!q || candidate.key.toLowerCase().startsWith(q)) return true;\n const title = candidate.title.toLowerCase();\n const words = q.split(/[\\s_-]+/).filter(Boolean);\n return words.length > 0 && words.every((w) => title.includes(w));\n}\n\n/** How many conversations the menu remembers to match locally (`rememberCandidates`). */\nexport const REMEMBERED_CANDIDATES_LIMIT = 200;\n\nconst sameCandidate = (a: ChatConversationReferenceCandidate, b: ChatConversationReferenceCandidate) =>\n a.id === b.id && a.key === b.key && a.title === b.title && a.isOwn === b.isOwn && a.updatedAt === b.updatedAt;\n\n/**\n * *seen* with *items* added: the latest version of each conversation, most\n * recent last, at most `REMEMBERED_CANDIDATES_LIMIT`. *seen* itself when every\n * item is already there as is, so a state holding it does not re-render.\n */\nexport function rememberCandidates(\n seen: ChatConversationReferenceCandidate[],\n items: ChatConversationReferenceCandidate[],\n): ChatConversationReferenceCandidate[] {\n if (items.every((item) => seen.some((c) => sameCandidate(c, item)))) return seen;\n const ids = new Set(items.map((c) => c.id));\n const next = [...seen.filter((c) => !ids.has(c.id)), ...items];\n return next.length > REMEMBERED_CANDIDATES_LIMIT ? next.slice(next.length - REMEMBERED_CANDIDATES_LIMIT) : next;\n}\n\n/**\n * *seen* after a search answer: the rows a settled answer should have listed\n * and did not are forgotten (deleted or unshared since), then its rows are\n * remembered. An absence is proven only under the narrowest contract every\n * backend keeps - the title contains `q` as typed - and by a complete answer\n * (under a page): a backend that does not match by key yet answers `hi-bob`\n * with nothing, and \"Hi Bob\" must stay. The conversation written in is\n * excluded by the request, not gone.\n */\nexport function rememberAnswer(\n seen: ChatConversationReferenceCandidate[],\n search: ReferenceSearchState,\n exclude?: string | null,\n): ChatConversationReferenceCandidate[] {\n if (search.query === null || search.failed) return seen;\n const query = search.query;\n const listed = new Set(search.items.map((c) => c.id));\n const complete = search.items.length < REFERENCE_MENU_LIMIT;\n const q = query.toLowerCase();\n const kept = complete ? seen.filter((c) => c.id === exclude || listed.has(c.id) || !c.title.toLowerCase().includes(q)) : seen;\n return rememberCandidates(kept.length === seen.length ? seen : kept, search.items);\n}\n\n/** A pick of the draft as a menu row; a pick that does not say whose it is reads as the user's own. */\nexport function refAsCandidate(ref: ChatConversationRef): ChatConversationReferenceCandidate {\n return { id: ref.conversationId, title: ref.title, key: ref.key, isOwn: ref.isOwn ?? true };\n}\n\n/** What the menu shows. */\nexport type ReferenceMenuView =\n | { kind: 'closed' }\n | { kind: 'results'; items: ChatConversationReferenceCandidate[] }\n /** A typed query nothing matches. */\n | { kind: 'empty' }\n /** The message already references as many conversations as it may. */\n | { kind: 'full' };\n\n/**\n * The menu for the `@` being typed.\n *\n * The previous answer stays on screen while the next one loads, so typing\n * never empties the menu; \"no match\" is said only once the backend answered\n * the very text typed, and only for a text: an empty `@` with nothing to offer\n * is simply no menu.\n */\nexport function referenceMenuView(options: {\n trigger: ReferenceTrigger | null;\n dismissed: boolean;\n search: ReferenceSearchState;\n /** How many conversations the text already references. */\n referenced: number;\n /** The conversation written in, never offered. */\n exclude?: string | null;\n /**\n * The conversations already met (the menu's past answers, the draft's\n * picks), matched here by key or title after the search's own answer: an\n * edited `@hi-bob`, or one a keystroke broke, still finds \"Hi Bob\" whatever\n * the search answered.\n */\n known?: ChatConversationReferenceCandidate[];\n}): ReferenceMenuView {\n const { trigger, dismissed, search, referenced, exclude, known = [] } = options;\n if (!trigger || dismissed) return { kind: 'closed' };\n if (referenced >= MAX_REFERENCED_CONVERSATIONS) return { kind: 'full' };\n if (search.failed) return { kind: 'closed' };\n const local = trigger.query ? known.filter((c) => referenceMatches(trigger.query, c) && !search.items.some((s) => s.id === c.id)) : [];\n const merged = [...search.items, ...local].slice(0, REFERENCE_MENU_LIMIT);\n const items = exclude ? merged.filter((c) => c.id !== exclude) : merged;\n if (items.length > 0) return { kind: 'results', items };\n if (trigger.query && search.query === trigger.query) return { kind: 'empty' };\n return { kind: 'closed' };\n}\n","import type { ChatConversationSummary, ChatWorkspace } from '../types';\n\n/**\n * The conversation list grouped by workspace, the one rule a host's own list\n * shares through `@filigran/chatbot/workspaces`. No local runtime imports, so\n * `node --test` runs it as is.\n */\n\n/** The key of the group of conversations filed in a workspace the list does not show. */\nexport const ELSEWHERE_GROUP = '__elsewhere__';\n\n/** What the grouping reads of a conversation; a host's own type passes through whole. */\nexport type GroupableConversation = Pick<ChatConversationSummary, 'conversationId' | 'workspaceId'>;\n\nexport interface WorkspaceGroup<C extends GroupableConversation = ChatConversationSummary, W extends ChatWorkspace = ChatWorkspace> {\n /** `null` for the `elsewhere` group. */\n workspace: W | null;\n /** The workspace's id, or `ELSEWHERE_GROUP`. */\n key: string;\n conversations: C[];\n}\n\n/**\n * Defensive parse of the workspace list (a bare array or `{ workspaces }`):\n * entries without an id or a name are skipped, and so are archived ones,\n * which take no conversation. A workspace is the caller's own by its\n * `is_own`, or, where the backend sends none, by its `user_id` being\n * *userId*.\n */\nexport function parseWorkspaces(data: unknown, options: { userId?: string } = {}): ChatWorkspace[] {\n const list = Array.isArray(data)\n ? data\n : Array.isArray((data as Record<string, unknown> | null)?.workspaces)\n ? ((data as Record<string, unknown>).workspaces as unknown[])\n : [];\n const out: ChatWorkspace[] = [];\n for (const raw of list) {\n if (!raw || typeof raw !== 'object') continue;\n const w = raw as Record<string, unknown>;\n if (typeof w.id !== 'string' || !w.id || typeof w.name !== 'string') continue;\n // An archived workspace takes no conversation; the backend already leaves\n // it out, and a proxy that did not would offer a choice that can only fail.\n if (w.is_archived === true) continue;\n out.push({\n id: w.id,\n name: w.name,\n isOwn: typeof w.is_own === 'boolean' ? w.is_own : !!options.userId && w.user_id === options.userId,\n isDefault: w.is_default === true,\n canManage: w.can_manage === true,\n });\n }\n return out;\n}\n\n/** The caller's own default first, then by name. */\nexport function sortWorkspaces<W extends ChatWorkspace>(workspaces: W[]): W[] {\n const mine = (w: ChatWorkspace) => w.isDefault && w.isOwn;\n return [...workspaces].sort((a, b) => {\n if (mine(a) !== mine(b)) return mine(a) ? -1 : 1;\n return a.name.localeCompare(b.name);\n });\n}\n\n/**\n * Whether a failed read of the workspace list is a passing one (a timeout, a\n * rate limit, a server error) that keeps the last list, rather than a refusal\n * (not licensed, an older proxy) that turns the grouping off.\n */\nexport function isTransientListFailure(status: number): boolean {\n return status === 408 || status === 429 || (status >= 500 && status !== 501);\n}\n\n/** Whether moving *conversationId* to *destination* would change nothing (it is already there). */\nexport function isNoopMove(\n conversations: Pick<ChatConversationSummary, 'conversationId' | 'workspaceId'>[],\n conversationId: string,\n destination: string | null,\n): boolean {\n const current = conversations.find((c) => c.conversationId === conversationId);\n return !!current && (current.workspaceId ?? null) === destination;\n}\n\n/**\n * Whether the next conversation can still be created in *pendingId*: the list\n * read last offers it to file into. One deleted, archived or unshared since\n * (from another tab, the web chat) is not, and the session request must not\n * name it.\n */\nexport function pendingWorkspaceStillFileable(pendingId: string, workspaces: ChatWorkspace[]): boolean {\n return workspaces.some((w) => w.id === pendingId && w.canManage);\n}\n\n/** The shortest gap between two re-reads on a return to the page (`focus` and `visibilitychange` both fire). */\nexport const RETURN_REFRESH_MIN_GAP_MS = 2000;\n\n/** Whether a return to the page re-reads the lists: once per return, not once per event. */\nexport function returnRefreshDue(now: number, last: number | null): boolean {\n return last === null || now - last >= RETURN_REFRESH_MIN_GAP_MS;\n}\n\n/** The workspaces a conversation can be moved into, in the order the list shows them. */\nexport function fileableWorkspaces<W extends ChatWorkspace>(workspaces: W[]): W[] {\n return sortWorkspaces(workspaces.filter((w) => w.canManage));\n}\n\n/**\n * The groups of the conversation list, in order, and the conversations filed\n * in none: a group for each workspace the caller may file into, empty or not,\n * and for one they may only read while it holds a conversation of theirs;\n * the conversations filed in a workspace not listed go to one `ELSEWHERE_GROUP`.\n * With *matches* (a search) only the matching conversations are kept, and\n * the groups left empty go.\n */\nexport function groupConversations<C extends GroupableConversation, W extends ChatWorkspace>(\n conversations: C[],\n workspaces: W[],\n options: { matches?: (c: C) => boolean } = {},\n): { groups: WorkspaceGroup<C, W>[]; unfiled: C[] } {\n const { matches } = options;\n const shown = matches ? conversations.filter(matches) : conversations;\n const known = new Set(workspaces.map((w) => w.id));\n const held = new Set(conversations.map((c) => c.workspaceId).filter((id): id is string => !!id));\n\n const byWorkspace = new Map<string, C[]>();\n const elsewhere: C[] = [];\n const unfiled: C[] = [];\n for (const c of shown) {\n const id = c.workspaceId ?? null;\n if (!id) unfiled.push(c);\n else if (known.has(id)) byWorkspace.set(id, [...(byWorkspace.get(id) ?? []), c]);\n else elsewhere.push(c);\n }\n\n const groups: WorkspaceGroup<C, W>[] = [];\n for (const workspace of sortWorkspaces(workspaces)) {\n const inside = byWorkspace.get(workspace.id) ?? [];\n if (!workspace.canManage && !held.has(workspace.id)) continue;\n if (matches && inside.length === 0) continue;\n groups.push({ workspace, key: workspace.id, conversations: inside });\n }\n if (elsewhere.length > 0) groups.push({ workspace: null, key: ELSEWHERE_GROUP, conversations: elsewhere });\n return { groups, unfiled };\n}\n\n/**\n * Whether a group shows its conversations: not when collapsed, unless it holds\n * the conversation on screen, and always while searching. A group shown open\n * for either reason cannot be toggled (`groupCanToggle`).\n */\nexport function groupIsOpen(\n key: string,\n collapsed: ReadonlySet<string>,\n group: { conversations: GroupableConversation[] },\n activeConversationId: string | null,\n searching: boolean,\n): boolean {\n if (searching) return true;\n if (activeConversationId && group.conversations.some((c) => c.conversationId === activeConversationId)) return true;\n return !collapsed.has(key);\n}\n\n/**\n * Whether a group's toggle may fold it: not while it is held open by a search\n * or by the conversation on screen, where a click would flip a remembered\n * state nobody sees, which then folds the group later.\n */\nexport function groupCanToggle(group: { conversations: GroupableConversation[] }, activeConversationId: string | null, searching: boolean): boolean {\n if (searching) return false;\n return !(activeConversationId && group.conversations.some((c) => c.conversationId === activeConversationId));\n}\n\n/**\n * Undo one refused move in the conversation list: the conversation goes back\n * to where it was, and only while it is still where this move put it - once a\n * later move or a refresh has placed it, that placement stands. Every other\n * row is kept as the list holds it now, not as it was when the move started.\n */\nexport function revertMove<T extends Pick<ChatConversationSummary, 'conversationId' | 'workspaceId'>>(\n list: T[],\n conversationId: string,\n destination: string | null,\n previous: string | null | undefined,\n): T[] {\n const index = list.findIndex((c) => c.conversationId === conversationId);\n if (index < 0 || (list[index].workspaceId ?? null) !== destination) return list;\n const next = [...list];\n next[index] = { ...list[index], workspaceId: previous };\n return next;\n}\n\n/**\n * What an empty \"Not in a workspace\" group says: that there is no\n * conversation at all, that every one is filed, or nothing. Nothing while\n * searching: the group then holds matches only, and a search that matches\n * nothing has its own message.\n */\nexport function unfiledEmptyNote(options: { total: number; unfiled: number; loading: boolean; searching: boolean }): 'none' | 'all-filed' | null {\n const { total, unfiled, loading, searching } = options;\n if (unfiled > 0 || loading || searching) return null;\n return total === 0 ? 'none' : 'all-filed';\n}\n","/**\n * What a backend says when it refuses a request, and how the panel tells a\n * new set of credentials from the one refused.\n */\n\n/** Long enough for a sentence, short enough that an error page is never shown. */\nconst MAX_DETAIL_LENGTH = 500;\n\nconst usable = (value: unknown): string | null => {\n if (typeof value !== 'string') return null;\n const text = value.trim();\n return text && text.length <= MAX_DETAIL_LENGTH ? text : null;\n};\n\n/**\n * The message of a refusal body: its `detail` when a string, or that object's\n * `message` (or `detail`); null when there is none worth showing.\n */\nexport function refusalDetail(body: unknown): string | null {\n if (!body || typeof body !== 'object') return null;\n const detail = (body as Record<string, unknown>).detail;\n if (detail && typeof detail === 'object' && !Array.isArray(detail)) {\n const inner = detail as Record<string, unknown>;\n return usable(inner.message) ?? usable(inner.detail);\n }\n return usable(detail);\n}\n\n/** The message of a refused response's JSON body, null when it has none (or is not JSON). */\nexport async function readRefusalDetail(res: Response): Promise<string | null> {\n try {\n return refusalDetail(await res.json());\n } catch {\n return null;\n }\n}\n\n/** A key that changes exactly when the request headers do, whatever their order. */\nexport function requestHeadersKey(headers: Record<string, string> | undefined): string {\n if (!headers) return '';\n return JSON.stringify(Object.entries(headers).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));\n}\n","/**\n * A random UUID. `crypto.randomUUID` exists only in secure contexts, so a page\n * served over plain http (other than localhost) builds one from random bytes.\n */\nexport function randomId(): string {\n if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n","import type {\n ChatAnsweredBy,\n ChatArtifact,\n ChatArtifactKind,\n ChatAttachment,\n ChatContextBreakdown,\n ChatContextUsage,\n ChatConversationRef,\n ChatMessage,\n ChatMessagePersistedFeedback,\n ToolApprovalProposal,\n ToolCallTraceEntry,\n TransferChainEntry,\n} from '../../types';\nimport type { PageAction } from '../../utils/pageActions';\nimport type { ParsedAction, ProtocolContext } from './types';\nimport { randomId } from '../../utils/randomId';\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\nconst ARTIFACT_KINDS: readonly ChatArtifactKind[] = ['presentation', 'report', 'video'];\n\nconst countOf = (value: unknown): number | undefined => (typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined);\n\n/**\n * Normalize one artifact descriptor (`artifacts` entry, or the answer of\n * `GET {artifacts}/{kind}/{id}`). `undefined` without a known kind and an id:\n * a card it could not address would offer actions that go nowhere.\n */\nexport function parseArtifact(raw: unknown): ChatArtifact | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const a = raw as Record<string, unknown>;\n const kind = ARTIFACT_KINDS.find((k) => k === a.kind);\n if (!kind || typeof a.id !== 'string' || !a.id) return undefined;\n return {\n kind,\n id: a.id,\n title: typeof a.title === 'string' ? a.title : '',\n url: typeof a.url === 'string' && a.url ? a.url : undefined,\n slideRef: typeof a.slide_ref === 'string' && a.slide_ref ? a.slide_ref : undefined,\n slideCount: countOf(a.slide_count),\n sectionCount: countOf(a.section_count),\n sceneCount: countOf(a.scene_count),\n durationMs: countOf(a.duration_ms),\n status: typeof a.status === 'string' && a.status ? a.status : undefined,\n progress: countOf(a.progress),\n downloads: Array.isArray(a.downloads) ? a.downloads.filter((f): f is string => typeof f === 'string' && !!f) : [],\n };\n}\n\n/** Normalize the `artifacts` of a `done` event or a restored answer; `undefined` when nothing is usable. */\nexport function parseArtifacts(raw: unknown): ChatArtifact[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out = raw.map(parseArtifact).filter((a): a is ChatArtifact => !!a);\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 `conversation_refs` of a restored user message: the\n * conversations it referenced with `@`, as the backend recorded them. An entry\n * without a conversation id is skipped; `undefined` when nothing is left.\n */\nexport function parseConversationRefs(raw: unknown): ChatConversationRef[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: ChatConversationRef[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const r = item as Record<string, unknown>;\n if (typeof r.conversation_id !== 'string' || !r.conversation_id) continue;\n if (out.some((o) => o.conversationId === r.conversation_id)) continue;\n out.push({\n conversationId: r.conversation_id,\n title: typeof r.title === 'string' ? r.title.trim() : '',\n key: typeof r.key === 'string' ? r.key : '',\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 // Left empty for the UI to label: a parser has no `t`, and a placeholder\n // baked in here would reach the reviewer untranslated.\n toolName: typeof p.tool_name === 'string' && p.tool_name ? p.tool_name : '',\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/** The page action of an `act_on_page` frame; anything unreadable becomes `unsupported`, never nothing. */\nfunction parsePageAction(raw: unknown): PageAction {\n if (!raw || typeof raw !== 'object') return { type: 'unsupported', requested: 'malformed' };\n const a = raw as Record<string, unknown>;\n const str = (v: unknown) => typeof v === 'string' && v.length > 0;\n if (a.type === 'snapshot') return { type: 'snapshot' };\n if (a.type === 'navigate' && str(a.path)) return { type: 'navigate', path: a.path as string };\n if (a.type === 'click' && str(a.ref)) return { type: 'click', ref: a.ref as string };\n if (a.type === 'open' && str(a.ref)) return { type: 'open', ref: a.ref as string };\n if (a.type === 'type' && str(a.ref) && typeof a.text === 'string') {\n return { type: 'type', ref: a.ref as string, text: a.text };\n }\n // Answered, not dropped: the backend holds the turn open until the action\n // comes back, so an unknown shape would stall until its timeout.\n return { type: 'unsupported', requested: typeof a.type === 'string' ? a.type.slice(0, 64) : 'unknown' };\n}\n\nconst optionalString = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined);\n\n/**\n * `answered_by`: an agent object (`agent_id`, `agent_name`, `agent_icon`), or\n * a bare name. Undefined when it names nothing.\n */\nexport function parseAnsweredBy(raw: unknown): ChatAnsweredBy | undefined {\n if (typeof raw === 'string') return raw ? { agentName: raw } : undefined;\n if (!raw || typeof raw !== 'object') return undefined;\n const a = raw as Record<string, unknown>;\n const by: ChatAnsweredBy = {\n agentId: optionalString(a.agent_id),\n agentName: optionalString(a.agent_name) ?? optionalString(a.name),\n agentIcon: optionalString(a.agent_icon),\n };\n if (!by.agentId && !by.agentName) return undefined;\n return Object.fromEntries(Object.entries(by).filter(([, v]) => v !== undefined)) as ChatAnsweredBy;\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' || type === 'cancelled') {\n // A provisional end (a Stop's acknowledgement, a failed loop's error) says\n // so: the stored end of the same turn follows it. Any other is final.\n const messageId = optionalString(evt.message_id);\n const provisional = evt.provisional === true ? (true as const) : undefined;\n // A stored end describes the stored answer: the host reads it as it came.\n const end = { ...(messageId ? { messageId, raw: { ...evt } } : {}), ...(provisional ? { provisional } : {}) };\n return type === 'error' ? { action: 'error', content: (evt.content as string) || '', ...end } : { action: 'cancelled', ...end };\n }\n // The user's session ended: the backend closes the stream after this frame,\n // and no turn on screen can go on for this user.\n if (type === 'session_expired') return { action: 'session_ended', reason: optionalString(evt.reason) };\n if (type === 'approval_resolved') return { action: 'approval_resolved' };\n if (type === 'folded_into_active_turn') return { action: 'folded', turnId: optionalString(evt.turn_id) };\n if (type === 'conversation_title') {\n const title = optionalString(evt.title);\n return title ? { action: 'conversation_title', conversationId: optionalString(evt.conversation_id), title } : { action: 'noop' };\n }\n if (type === 'user_message') {\n const [message] = parseRestoredMessages([evt.message]);\n if (!message || message.role !== 'user') return { action: 'noop' };\n return { action: 'user_message', message: { ...message, id: storedMessageId(message.serverId) } };\n }\n // The native vocabulary acknowledges a turn with a top-level `thinking`\n // frame, which carries how long a resumed turn has run.\n if (type === 'thinking') {\n return {\n action: 'status',\n status: 'thinking',\n resumed: evt.resumed === true || undefined,\n elapsedS: typeof evt.elapsed_s === 'number' ? evt.elapsed_s : undefined,\n };\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_progress' && evt.event === 'page_action_required') {\n // Without an action_id there is nothing to answer against, so that frame\n // alone is dropped; every other shape is answered.\n if (typeof evt.action_id !== 'string' || !evt.action_id) return { action: 'noop' };\n const pageAction = parsePageAction(evt.action);\n return {\n action: 'page_action_required',\n actionId: evt.action_id,\n pageAction,\n conversationId: typeof evt.conversation_id === 'string' ? evt.conversation_id : undefined,\n };\n }\n if (st === 'wait_start') {\n return {\n action: 'status',\n status: 'wait_start',\n waitSeconds: typeof evt.seconds === 'number' && evt.seconds > 0 ? evt.seconds : undefined,\n waitReason: optionalString(evt.reason),\n };\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 artifacts: parseArtifacts(evt.artifacts),\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 messageId: typeof evt.message_id === 'string' && evt.message_id ? evt.message_id : undefined,\n intermediate: evt.intermediate === true || undefined,\n answeredBy: parseAnsweredBy(evt.answered_by),\n mentionFailed: evt.mention_failed === true || undefined,\n raw: { ...evt },\n };\n }\n\n return { action: 'noop' };\n}\n\n/**\n * The transcript id of a message the backend stored, however it reached the\n * screen (restored, relayed, read back), so no two rows show it; *unsaved*\n * (a fresh id by default) for one it did not.\n */\nexport function storedMessageId(serverId: string | undefined, unsaved: string = randomId()): string {\n return serverId ? `msg-${serverId}` : unsaved;\n}\n\n/** The `feedback` of a restored entry, or undefined for anything that is not a stored rating. */\nexport function parsePersistedFeedback(raw: unknown): ChatMessagePersistedFeedback | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const { rating, comment } = raw as { rating?: unknown; comment?: unknown };\n if (rating !== 'positive' && rating !== 'negative') return undefined;\n return { rating, comment: typeof comment === 'string' && comment.trim() ? comment : null };\n}\n\n/**\n * The messages of a session restore (`POST {sessions}` with a known\n * `conversation_id`), in order.\n *\n * Rows are keyed by the entry's own `id`, the message's persisted id (see\n * `storedMessageId`), kept as `serverId` for what is stored against it\n * (feedback); an entry without one by its index. Its `created_at` is when it was sent, and\n * a backend that sends neither still restores - with no time to show\n * (`timestampUnknown`) rather than the time of the restore.\n * Entries that are not a user or an assistant message are skipped.\n */\nexport function parseRestoredMessages(raw: unknown): ChatMessage[] {\n if (!Array.isArray(raw)) return [];\n const out: ChatMessage[] = [];\n raw.forEach((item: unknown, i: number) => {\n if (!item || typeof item !== 'object') return;\n const m = item as Record<string, unknown>;\n if (m.role !== 'user' && m.role !== 'assistant') return;\n const createdAt = typeof m.created_at === 'string' ? new Date(m.created_at) : null;\n const sentAt = createdAt && !Number.isNaN(createdAt.getTime()) ? createdAt : null;\n const answeredBy = m.role === 'assistant' ? parseAnsweredBy(m.answered_by) : undefined;\n const serverId = typeof m.id === 'string' && m.id ? m.id : undefined;\n out.push({\n id: storedMessageId(serverId, `restored-${i}`),\n serverId,\n role: m.role,\n content: typeof m.content === 'string' ? m.content : '',\n timestamp: sentAt ?? new Date(),\n timestampUnknown: sentAt ? undefined : true,\n feedback: m.role === 'assistant' ? parsePersistedFeedback(m.feedback) : undefined,\n // Per-message attribution: the agent that wrote an answer of a turn\n // several agents answered (as a live `done` names it), else the\n // message's own agent when the backend keeps one; undefined lets the\n // conversation's agent apply to the whole thread.\n agentName: answeredBy?.agentName ?? (typeof m.agent_name === 'string' ? m.agent_name : undefined),\n answeredBy,\n // Whether it is a note in place of an answer that could not be given, for the host.\n mentionFailed: m.role === 'assistant' && m.mention_failed === true ? true : undefined,\n // Downloadable file chips on both roles: agent-generated deliverables on\n // assistant messages (their [[FILE:…]] markers are stripped at render\n // time) and user uploads, so an upload stays downloadable after a reload\n // and not only in the live session where it is carried on `files`.\n attachments: parseAttachments(m.attachments),\n // The decks, reports and videos an answer made, as cards where their markers stand.\n artifacts: m.role === 'assistant' ? parseArtifacts(m.artifacts) : undefined,\n // The conversations a user message referenced with `@`, shown as chips.\n conversationRefs: m.role === 'user' ? parseConversationRefs(m.conversation_refs) : undefined,\n // The reasoning-details affordance, from the 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 // What the backend stores beyond what the panel reads, for the host.\n raw: { ...m },\n });\n });\n return out;\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 // Empty rather than a literal English fallback: the content is rendered as\n // the assistant's message, and the consumer supplies its own translated\n // text when the backend sends none — as the other protocols already do.\n return { action: 'error', content: typeof evt.message === 'string' ? evt.message : '' };\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","/**\n * The one session request of the chat on screen. A new chat abandons it: the\n * request is aborted, and `isCurrent()` turns false so a late answer touches\n * nothing. No local runtime imports, so `node --test` runs it as is.\n */\nexport interface SessionGate {\n /** Which chat is on screen; compare two readings to tell whether it changed. */\n readonly generation: number;\n /** The session request in flight for the chat on screen, if any. */\n readonly pending: Promise<string | null> | null;\n /**\n * Join the chat's request, or start it. A request that fails resolves to\n * `null`, like one abandoned.\n */\n run(request: (isCurrent: () => boolean, signal: AbortSignal) => Promise<string | null>): Promise<string | null>;\n /** A new chat: the request in flight, if any, is aborted and no longer its. */\n abandon(): void;\n}\n\nexport function createSessionGate(): SessionGate {\n let generation = 0;\n let pending: Promise<string | null> | null = null;\n let controller: AbortController | null = null;\n return {\n get generation() {\n return generation;\n },\n get pending() {\n return pending;\n },\n run(request) {\n if (pending) return pending;\n const started = generation;\n const own = new AbortController();\n controller = own;\n const promise = request(() => started === generation, own.signal).catch(() => null);\n pending = promise;\n // Released once settled, and only while it is still this request's:\n // after a new chat the next request may hold it already.\n void promise.finally(() => {\n if (pending === promise) pending = null;\n if (controller === own) controller = null;\n });\n return promise;\n },\n abandon() {\n generation += 1;\n pending = null;\n // The backend would otherwise still create the abandoned conversation,\n // filed in the workspace it was started in.\n controller?.abort();\n controller = null;\n },\n };\n}\n","/**\n * Performing one `act_on_page` action on the host page (#5559). Navigation goes\n * through the host's router, never a page load, which would end the chat turn —\n * so a link is followed by routing its href, not by clicking it.\n */\n\nexport type PageAction =\n | { type: 'snapshot' }\n | { type: 'navigate'; path: string }\n | { type: 'click'; ref: string }\n | { type: 'type'; ref: string; text: string }\n | { type: 'open'; ref: string }\n /** A frame naming an action this version does not know (backend ahead of the panel). */\n | { type: 'unsupported'; requested: string };\n\n/**\n * Whether the panel may declare `supports_page_actions` (#5559). Passing\n * `onRelativeLinkClick` is the host's opt-in: a proxy that relays tool\n * approvals but not page answers would stall every step on its timeout.\n */\nexport function hostTakesPageActions(approveUrl: string | null, onNavigate?: (path: string) => void): boolean {\n return approveUrl !== null && typeof onNavigate === 'function';\n}\n\n/** The document's own address, which every href is resolved against. */\nfunction baseHref(doc?: Document): string | undefined {\n return doc?.defaultView?.location?.href ?? (typeof window === 'undefined' ? undefined : window.location.href);\n}\n\n/**\n * The host-internal form of *href*, or null when it leads anywhere else.\n * Resolved and origin-compared even when it looks relative: `/\\evil.com` and\n * ` //evil.com` do not, and resolve to another origin.\n */\nexport const toInternalHref = (href?: string, base?: string): string | null => {\n const from = base ?? baseHref();\n if (!href || !from) return null;\n try {\n const url = new URL(href, from);\n if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;\n if (url.origin !== new URL(from).origin) return null;\n const path = `${url.pathname}${url.search}${url.hash}` || '/';\n // `/.//host/x` normalises to `//host/x`, which passes the origin check here\n // and then reads as protocol-relative to a host that resolves it again.\n return path.startsWith('//') ? null : path;\n } catch {\n return null;\n }\n};\n\n/**\n * Whether *href* is *written* as a relative reference. Not a safety boundary -\n * `toInternalHref` is - but the shape a link was authored in: a same-origin\n * absolute link still opens in a new tab when the host routes nothing.\n */\nexport const isRelativeHref = (href?: string): boolean => {\n if (!href) return false;\n if (href.startsWith('//')) return false;\n return !/^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(href);\n};\n\nexport interface PageActionResult {\n ok: boolean;\n error?: string;\n}\n\n/** The panel's own subtree, whose mutations must never count as the page reacting. */\nconst PANEL_SELECTOR = '.filigran-chatbot';\n\n/** How long a hover is given to open a menu. */\nconst HOVER_WAIT_MS = 300;\n\nfunction insidePanel(node: Node | null): boolean {\n // Duck-typed: `instanceof Element` throws where there is no such global, and\n // the observer swallows the throw, losing the mutation.\n const el = (node && 'closest' in node ? node : node?.parentElement) as Element | null | undefined;\n return !!el?.closest?.(PANEL_SELECTOR);\n}\n\n/**\n * Fields `type` may write to. A `<select>`, link or button would otherwise fall\n * through to `textContent` and lose its options or its children. Password is\n * left out on purpose: the panel neither reads nor fills credentials.\n */\nconst TYPEABLE =\n 'textarea, [contenteditable=\"true\"], input:not([type]), input[type=\"text\"], input[type=\"search\"], input[type=\"email\"], input[type=\"url\"], input[type=\"tel\"], input[type=\"number\"]';\n\nfunction isTypeable(el: Element): boolean {\n return el.matches?.(TYPEABLE) ?? false;\n}\n\n/**\n * Secret fields, by autocomplete. Mirrors `pageSnapshot`'s own test, which\n * cannot be shared: both modules are loaded by `node --test`, where a runtime\n * relative import does not resolve. `secretFieldsAgree` in the tests pins them\n * to the same answer.\n */\nconst SECRET_AUTOCOMPLETE = /^(current-password|new-password|one-time-code|cc-)/;\n\nexport function isSecretField(el: Element): boolean {\n if ((el.getAttribute('type') || '').toLowerCase() === 'password') return true;\n return (el.getAttribute('autocomplete') || '')\n .toLowerCase()\n .split(/\\s+/)\n .some((token) => SECRET_AUTOCOMPLETE.test(token));\n}\n\n/** Set *text* the way a user's typing does, so a React-controlled field sees it. */\nfunction setValue(el: Element, text: string): void {\n const view = el.ownerDocument.defaultView as (Window & typeof globalThis) | null;\n if (view && (el instanceof view.HTMLInputElement || el instanceof view.HTMLTextAreaElement)) {\n // React tracks the value it last rendered on the element itself; only the\n // prototype's setter goes around that tracker, so the input event below\n // reaches the component as a change.\n const proto = el instanceof view.HTMLInputElement ? view.HTMLInputElement.prototype : view.HTMLTextAreaElement.prototype;\n Object.getOwnPropertyDescriptor(proto, 'value')?.set?.call(el, text);\n } else {\n el.textContent = text;\n }\n el.dispatchEvent(new (view?.Event ?? Event)('input', { bubbles: true }));\n el.dispatchEvent(new (view?.Event ?? Event)('change', { bubbles: true }));\n}\n\n/** Hover *el* the way a pointer does: what opens a menu on hover opens. */\nfunction hover(el: Element): void {\n const view = el.ownerDocument.defaultView as (Window & typeof globalThis) | null;\n const init = { bubbles: true, cancelable: true, view: view ?? undefined, pointerType: 'mouse' };\n const Pointer = view?.PointerEvent ?? view?.MouseEvent ?? MouseEvent;\n const Mouse = view?.MouseEvent ?? MouseEvent;\n el.dispatchEvent(new Pointer('pointerover', init));\n el.dispatchEvent(new Pointer('pointerenter', { ...init, bubbles: false }));\n el.dispatchEvent(new Mouse('mouseover', init));\n el.dispatchEvent(new Mouse('mouseenter', { ...init, bubbles: false }));\n}\n\n/**\n * Whether *act* adds anything to the page within *ms*: a menu that opened.\n * The panel's own re-render (the \"Acting on the page…\" status) lands inside the\n * same window, so its subtree is ignored or every hover would look successful.\n */\nfunction addsContent(doc: Document, act: () => void, ms = HOVER_WAIT_MS): Promise<boolean> {\n const view = doc.defaultView as (Window & typeof globalThis) | null;\n const Observer = view?.MutationObserver ?? globalThis.MutationObserver;\n if (!Observer) {\n act();\n return Promise.resolve(true);\n }\n return new Promise((resolve) => {\n const observer = new Observer((records) => {\n const added = records.some(\n (r) => r.addedNodes.length > 0 && !insidePanel(r.target) && Array.from(r.addedNodes).some((n) => !insidePanel(n)),\n );\n if (!added) return;\n observer.disconnect();\n clearTimeout(timer);\n resolve(true);\n });\n observer.observe(doc.body ?? doc.documentElement, { subtree: true, childList: true });\n act();\n const timer = setTimeout(() => {\n observer.disconnect();\n resolve(false);\n }, ms);\n });\n}\n\n/**\n * Run *act*, cancelling the default action of whatever event it fires. Capture\n * phase, so handlers still run: the panel may activate a control but must never\n * cause the page load that would end the turn.\n */\nfunction withoutDefault(doc: Document, types: string[], act: () => void): void {\n const suppress = (event: Event) => event.preventDefault();\n for (const type of types) doc.addEventListener(type, suppress, true);\n try {\n act();\n } finally {\n for (const type of types) doc.removeEventListener(type, suppress, true);\n }\n}\n\n/**\n * Click *el*, routing an anchor through the host so the page is not left and\n * the turn ended. Only *el* itself: a control inside a link row has its own\n * handler, which routing the row would skip.\n */\nfunction clickOrRoute(el: Element, onNavigate?: (path: string) => void, base?: string): PageActionResult {\n if (el.matches?.('a[href]')) {\n // Before the fragment case below: `<a href=\"#\" target=\"_blank\">` still\n // opens a tab, which leaves the page the turn is acting on.\n const target = el.getAttribute('target');\n if (target && target !== '_self') return { ok: false, error: 'opens_new_tab' };\n // A fragment is an in-page trigger carrying a handler, not a destination,\n // so it is clicked like anything else.\n const href = el.getAttribute('href') ?? '';\n if (href && !href.startsWith('#')) {\n const internal = toInternalHref(href, base);\n if (!internal) return { ok: false, error: 'external_link' };\n if (onNavigate) {\n onNavigate(internal);\n return { ok: true };\n }\n }\n }\n // Focus first: a click moves no focus of its own, so a field edited just\n // before would never blur, and a host that commits on blur (a tag input)\n // would run this activation without the edit.\n (el as HTMLElement).focus?.();\n // A submit control loads its form's action, and a click anywhere inside an\n // anchor activates it - both end the turn. Their handlers still run.\n // A fragment moves within the page, so its default stands; anything else an\n // anchor does loads one.\n const anchor = el.closest?.('a[href]');\n const inAnchor = !!anchor && !(anchor.getAttribute('href') ?? '').startsWith('#');\n const cancels = inAnchor ? ['submit', 'click'] : ['submit'];\n const checkable = el.matches?.('input[type=\"checkbox\"], input[type=\"radio\"]') ?? false;\n const before = (el as HTMLInputElement).checked;\n withoutDefault(el.ownerDocument, cancels, () => (el as HTMLElement).click());\n // Cancelling the click also rolled back a checkable's own activation, which\n // is its behaviour rather than the anchor's: applied here instead.\n if (checkable && inAnchor && (el as HTMLInputElement).checked === before) {\n const input = el as HTMLInputElement;\n input.checked = input.type === 'radio' ? true : !before;\n input.dispatchEvent(new (el.ownerDocument.defaultView?.Event ?? Event)('input', { bubbles: true }));\n input.dispatchEvent(new (el.ownerDocument.defaultView?.Event ?? Event)('change', { bubbles: true }));\n }\n return { ok: true };\n}\n\n/** Resolve once *test* holds, or after *ms*. */\nfunction waitUntil(test: () => boolean, ms: number): Promise<boolean> {\n return new Promise((resolve) => {\n const started = Date.now();\n const tick = () => {\n if (test()) return resolve(true);\n if (Date.now() - started >= ms) return resolve(false);\n setTimeout(tick, 30);\n };\n tick();\n });\n}\n\n/** Whether *el* and every ancestor are rendered: a wrapper hides what it holds. */\nfunction visible(el: Element): boolean {\n const details = el.closest?.('details:not([open])');\n if (details && details !== el) {\n const summary = Array.from(details.children).find((c) => c.tagName.toLowerCase() === 'summary');\n if (!summary?.contains(el)) return false;\n }\n for (let node: Element | null = el; node; node = node.parentElement) {\n if ((node as HTMLElement).hidden || node.hasAttribute('inert')) return false;\n if (node.getAttribute('aria-hidden') === 'true') return false;\n const style = node.ownerDocument.defaultView?.getComputedStyle(node);\n if (style?.display === 'none' || style?.visibility === 'hidden') return false;\n }\n return true;\n}\n\n/** Whether the menu *el* controls is showing, by what the trigger says about it. */\nfunction menuShowing(el: Element): boolean {\n const expanded = el.getAttribute('aria-expanded');\n if (expanded === 'true') return true;\n // A stated collapsed state settles it, whatever the target looks like.\n if (expanded === 'false') return false;\n const id = el.getAttribute('aria-controls');\n if (!id) return false;\n const menu = el.ownerDocument.getElementById(id);\n return !!menu && visible(menu);\n}\n\n/**\n * Open the menu *el* triggers: by hover where that opens it, else by click.\n * A trigger stating its own state is believed; only one that says nothing\n * falls back to \"did the page grow\", which a tooltip would answer for it.\n */\nasync function openMenu(el: Element, base?: string, signal?: AbortSignal): Promise<PageActionResult> {\n const statesItself = el.hasAttribute('aria-expanded') || el.hasAttribute('aria-controls');\n if (menuShowing(el)) return { ok: true };\n if (statesItself) {\n // Waited on for its own state: anything else the hover adds, a tooltip,\n // says nothing about the menu and would cut the wait short.\n hover(el);\n if (await waitUntil(() => menuShowing(el), HOVER_WAIT_MS)) return { ok: true };\n } else if (await addsContent(el.ownerDocument, () => hover(el))) {\n return { ok: true };\n }\n // The turn may have been stopped during the hover wait; the click below would\n // land on the user's page after they asked for it to end.\n if (signal?.aborted) return { ok: false, error: 'aborted' };\n // No router: a trigger that happens to be an anchor must run its own click\n // handler rather than navigate to its href. The link guards still apply.\n return clickOrRoute(el, undefined, base);\n}\n\n/** Everything in a form a user fills in, for telling a search from a dialog. */\nconst SUBMITTABLE =\n 'input:not([type=\"hidden\"]):not([type=\"submit\"]):not([type=\"button\"]):not([type=\"reset\"]):not([type=\"image\"]), select, textarea, [contenteditable=\"true\"]';\n\n/** Words a search field is labelled with, where a create dialog's is not. */\nconst SEARCH_WORDS = /\\b(search|query|filter|find|recherche|rechercher)\\b/i;\n\n/**\n * Whether Enter in *el* asks for what was typed. Only a field that says it is a\n * search: a form of one field is just as likely a create or rename dialog, and\n * submitting it would commit the change the user has not asked for yet.\n */\nfunction entersSearch(el: Element): boolean {\n if (el.getAttribute('role') === 'searchbox') return true;\n if ((el as HTMLInputElement).type === 'search') return true;\n if (el.closest('[role=\"search\"]') !== null) return true;\n if (!['name', 'id', 'placeholder', 'aria-label', 'title'].some((attr) => SEARCH_WORDS.test(el.getAttribute(attr) ?? ''))) return false;\n // Named like a search, but a \"Filter name\" beside other fields is a dialog\n // saving a filter, not a search: only the form's sole field is sent.\n const form = (el as HTMLInputElement).form;\n return !form || form.querySelectorAll(SUBMITTABLE).length === 1;\n}\n\nfunction pressEnter(el: Element): void {\n const view = el.ownerDocument.defaultView as (Window & typeof globalThis) | null;\n const Keyboard = view?.KeyboardEvent ?? KeyboardEvent;\n const init = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };\n const down = new Keyboard('keydown', init);\n el.dispatchEvent(down);\n el.dispatchEvent(new Keyboard('keyup', init));\n // A synthetic key event never triggers the browser's implicit submission, so\n // a form the field belongs to is submitted unless a handler took the key.\n // Its native submission is cancelled: `requestSubmit` still validates and\n // still reaches the host's own handler, but cannot unload the page.\n const form = (el as HTMLInputElement).form;\n if (form && !down.defaultPrevented) {\n withoutDefault(el.ownerDocument, ['submit'], () => form.requestSubmit?.());\n }\n}\n\n/**\n * Mirrors `pageSnapshot`'s own test, which cannot be shared: both modules are\n * loaded by `node --test`, where a runtime relative import does not resolve.\n * `disabledTestsAgree` in the tests pins them to the same answer.\n */\nexport function isDisabled(el: Element): boolean {\n // The attribute as authored, so what the snapshot prints `(disabled)` for is\n // what is refused here - a custom `<a disabled>` included.\n if (el.hasAttribute('disabled')) return true;\n if (el.getAttribute('aria-disabled') === 'true') return true;\n if (el.matches?.(':disabled')) return true;\n // Only the native form controls a `<fieldset>` governs: a link or an ARIA\n // widget inside a disabled one still works. Its first `<legend>` is exempt.\n if (!el.matches?.('button, input, select, textarea')) return false;\n const fieldset = el.closest?.('fieldset[disabled]');\n if (!fieldset) return false;\n const legend = Array.from(fieldset.children).find((c) => c.tagName.toLowerCase() === 'legend');\n return !legend?.contains(el);\n}\n\n/** A field the user cannot edit either: the native setter would still write to it. */\nfunction isReadOnly(el: Element): boolean {\n return (el as HTMLInputElement).readOnly === true || el.getAttribute('aria-readonly') === 'true';\n}\n\nexport async function performPageAction(\n action: PageAction,\n refs: Map<string, Element>,\n onNavigate?: (path: string) => void,\n doc?: Document,\n signal?: AbortSignal,\n): Promise<PageActionResult> {\n try {\n if (signal?.aborted) return { ok: false, error: 'aborted' };\n if (action.type === 'snapshot') return { ok: true };\n if (action.type === 'unsupported') return { ok: false, error: `unsupported_action: ${action.requested}`.slice(0, 500) };\n if (action.type === 'navigate') {\n if (!onNavigate) return { ok: false, error: 'navigation_unavailable' };\n const path = toInternalHref(action.path, baseHref(doc));\n if (!path) return { ok: false, error: 'external_path' };\n onNavigate(path);\n return { ok: true };\n }\n const el = refs.get(action.ref);\n if (!el || !el.isConnected) return { ok: false, error: 'unknown_ref' };\n // Still in the document is not still reachable: a dialog may have closed or\n // an ancestor gone inert since the snapshot named it.\n if (!visible(el)) return { ok: false, error: 'not_visible' };\n if (isDisabled(el)) return { ok: false, error: 'disabled' };\n // The file chooser is an operating-system window: it cannot be driven from\n // here, and the turn would wait behind it.\n if (el.matches?.('input[type=\"file\"]')) return { ok: false, error: 'opens_file_dialog' };\n if (action.type === 'open') return await openMenu(el, baseHref(doc ?? el.ownerDocument), signal);\n if (action.type === 'click') return clickOrRoute(el, onNavigate, baseHref(doc ?? el.ownerDocument));\n if (!isTypeable(el)) return { ok: false, error: 'not_a_text_field' };\n if (isReadOnly(el)) return { ok: false, error: 'read_only' };\n // A revealed password or one-time code is masked in the snapshot; writing\n // to it would be filling a credential the panel will not read back.\n if (isSecretField(el)) return { ok: false, error: 'secret_field' };\n (el as HTMLElement).focus?.();\n setValue(el, action.text);\n if (entersSearch(el)) pressEnter(el);\n return { ok: true };\n } catch (err) {\n return { ok: false, error: `failed: ${err instanceof Error ? err.message : String(err)}`.slice(0, 500) };\n }\n}\n\n/**\n * Resolve once the page has not changed for *quietMs*, or after *maxMs* at most.\n * The panel's own subtree is ignored: its status re-renders would otherwise keep\n * resetting the quiet timer until the cap.\n */\nexport function waitForDomSettle(\n doc: Document,\n { quietMs = 300, maxMs = 3000, signal }: { quietMs?: number; maxMs?: number; signal?: AbortSignal } = {},\n): Promise<void> {\n return new Promise((resolve) => {\n // Before anything is set up: a stopped turn has nothing to wait for.\n if (signal?.aborted) return resolve();\n const view = doc.defaultView as (Window & typeof globalThis) | null;\n const Observer = view?.MutationObserver ?? globalThis.MutationObserver;\n let quiet: ReturnType<typeof setTimeout>;\n const done = () => {\n clearTimeout(quiet);\n clearTimeout(cap);\n observer?.disconnect();\n signal?.removeEventListener('abort', done);\n resolve();\n };\n const observer = Observer\n ? new Observer((records) => {\n if (!records.some((r) => !insidePanel(r.target))) return;\n clearTimeout(quiet);\n quiet = setTimeout(done, quietMs);\n })\n : null;\n observer?.observe(doc.body ?? doc.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });\n quiet = setTimeout(done, quietMs);\n const cap = setTimeout(done, maxMs);\n signal?.addEventListener('abort', done);\n });\n}\n","/**\n * The host page as `act_on_page` reads it (#5559): one line per element a user\n * could act on, keyed by a reference the next action names it by. The panel\n * itself is never part of it. The line format is in the README, under \"Acting\n * on the host page\".\n */\n\nexport const MAX_SNAPSHOT_CHARS = 20_000;\nconst MAX_NAME_CHARS = 80;\nconst MAX_URL_CHARS = 500;\nconst TRUNCATION_MARKER = '[truncated]';\n\nexport interface PageSnapshot {\n text: string;\n url: string;\n title: string;\n /** Reference → element, valid until the next snapshot. */\n refs: Map<string, Element>;\n}\n\nconst EXPLICIT_ROLES = new Set([\n 'button',\n 'link',\n 'tab',\n 'menuitem',\n 'menuitemcheckbox',\n 'menuitemradio',\n 'option',\n 'checkbox',\n 'radio',\n 'switch',\n 'textbox',\n 'searchbox',\n 'combobox',\n 'treeitem',\n]);\n\n/** Anything that could carry an actionable role, for the descent test below. */\nconst MAYBE_ACTIONABLE = 'a[href], button, summary, select, textarea, input, [contenteditable=\"true\"], [role]';\n\n/**\n * Whether an actionable element wraps further controls, which need references\n * of their own. A plain link is named by its contents; a `<select>`'s are not\n * controls.\n */\nfunction wrapsControls(el: Element): boolean {\n if (el.tagName.toLowerCase() === 'select') return false;\n for (const child of Array.from(el.querySelectorAll(MAYBE_ACTIONABLE))) {\n if (actionableRole(child)) return true;\n }\n return false;\n}\n\n/** The role an element is acted on as, or `null` when it is not actionable. */\nfunction actionableRole(el: Element): string | null {\n const explicit = el.getAttribute('role');\n if (explicit && EXPLICIT_ROLES.has(explicit)) return explicit;\n const tag = el.tagName.toLowerCase();\n if (tag === 'a' && el.hasAttribute('href')) return 'link';\n if (tag === 'button' || tag === 'summary') return 'button';\n if (tag === 'select') return 'combobox';\n if (tag === 'textarea') return 'textbox';\n if (tag === 'input') {\n const type = (el.getAttribute('type') || 'text').toLowerCase();\n if (type === 'hidden') return null;\n if (['button', 'submit', 'reset', 'image'].includes(type)) return 'button';\n if (type === 'checkbox' || type === 'radio') return type;\n if (type === 'search') return 'searchbox';\n // Opens a chooser rather than taking text; calling it a textbox invites a\n // `type` that is refused anyway.\n if (type === 'file') return 'button';\n return 'textbox';\n }\n if (el.getAttribute('contenteditable') === 'true') return 'textbox';\n return null;\n}\n\nfunction headingLevel(el: Element): number | null {\n const m = /^h([1-6])$/i.exec(el.tagName);\n return m ? Number(m[1]) : null;\n}\n\n/**\n * The element's text with a space wherever a non-inline element starts or\n * ends. `textContent` glues a table row's cells together (\"AdministratorZone de\n * danger1\"), and a name whose first cell is not a word cannot be matched.\n */\nfunction spacedText(el: Element): string {\n const view = el.ownerDocument.defaultView;\n let out = '';\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === 3) {\n out += node.textContent ?? '';\n } else if (node.nodeType === 1) {\n // The walk skips hidden subtrees, and a secret's value is masked; without\n // the same tests here both return as part of an ancestor's name, its\n // value, or a heading above them.\n if (hidden(node as Element) || isSecret(node as Element)) continue;\n const display = view?.getComputedStyle(node as Element).display ?? '';\n // An unknown display (no layout engine) is read as inline: never split a word.\n const inline = display === '' || display === 'inline' || display === 'contents';\n out += inline ? spacedText(node as Element) : ` ${spacedText(node as Element)} `;\n }\n }\n return out;\n}\n\nfunction clean(text: string | null | undefined): string {\n return (text ?? '').replace(/\\s+/g, ' ').trim();\n}\n\n/** Roles named by what they contain, where `title` is only a last resort. */\nconst NAMED_BY_CONTENT = new Set(['button', 'link', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'treeitem', 'switch']);\n\n/** The name a user reads for *el*, in the accessible-name order. */\nfunction accessibleName(el: Element, role: string): string {\n const doc = el.ownerDocument;\n // `aria-labelledby` outranks `aria-label`, and is read even when hidden: a\n // visually-hidden span is how an icon button usually gets its name.\n const labelledBy = el.getAttribute('aria-labelledby');\n if (labelledBy) {\n const text = clean(\n labelledBy\n .split(/\\s+/)\n .map((id) => doc.getElementById(id)?.textContent ?? '')\n .join(' '),\n );\n if (text) return text;\n }\n const fromAttr = clean(el.getAttribute('aria-label'));\n if (fromAttr) return fromAttr;\n const id = el.getAttribute('id');\n const label = (id && Array.from(doc.getElementsByTagName('label')).find((l) => l.htmlFor === id)) || el.closest('label');\n if (label) {\n // The label's own words, not the options of a select it wraps.\n const own = clean(\n Array.from(label.childNodes)\n .filter((n) => n.nodeType === 3)\n .map((n) => n.textContent)\n .join(' '),\n );\n if (own || clean(label.textContent)) return own || clean(label.textContent);\n }\n // An `<input>` button carries its label in `value`, not in any content.\n const tag = el.tagName.toLowerCase();\n const type = (el.getAttribute('type') || '').toLowerCase();\n if (tag === 'input' && ['submit', 'reset', 'button'].includes(type)) {\n const value = clean(el.getAttribute('value'));\n if (value) return value;\n if (type !== 'button') return type === 'submit' ? 'Submit' : 'Reset';\n }\n const alt = clean(el.getAttribute('alt')) || clean(el.querySelector('img[alt]')?.getAttribute('alt'));\n if (alt) return alt;\n // Its contents would hand over what `value()` just masked.\n const contents = isSecret(el) ? '' : clean(spacedText(el));\n // A control named by what it shows: the visible words beat a `title`, which\n // often holds a shortcut rather than a label.\n if (NAMED_BY_CONTENT.has(role) && contents) return contents;\n const placeholder = clean(el.getAttribute('placeholder'));\n if (placeholder) return placeholder;\n const title = clean(el.getAttribute('title'));\n if (title) return title;\n return contents;\n}\n\nfunction quote(text: string): string {\n // JSON quoting, so a newline in a textarea value cannot split one control\n // across lines or make its text read as another `[e2] button \"…\"` row.\n return JSON.stringify(text.slice(0, MAX_NAME_CHARS));\n}\n\n/**\n * Whether an ancestor `<fieldset disabled>` disables *el*. Only the native form\n * controls it governs - a link or an ARIA widget inside one still works - and\n * never what sits in its first `<legend>`.\n */\nfunction disabledByFieldset(el: Element): boolean {\n if (!el.matches?.('button, input, select, textarea')) return false;\n const fieldset = el.closest?.('fieldset[disabled]');\n if (!fieldset) return false;\n const legend = Array.from(fieldset.children).find((c) => c.tagName.toLowerCase() === 'legend');\n return !legend?.contains(el);\n}\n\nfunction states(el: Element): string[] {\n const out: string[] = [];\n if (el.hasAttribute('disabled') || disabledByFieldset(el) || el.getAttribute('aria-disabled') === 'true') out.push('disabled');\n if (el.getAttribute('aria-selected') === 'true') out.push('selected');\n if ((el as HTMLInputElement).checked || el.getAttribute('aria-checked') === 'true') out.push('checked');\n if (el.getAttribute('aria-expanded') === 'true') out.push('expanded');\n // A menu that opens on hover or click and is closed now: its items are not\n // in the page until it opens (`open` action), so the snapshot says it is there.\n else if ((el.getAttribute('aria-haspopup') ?? 'false') !== 'false') out.push('menu');\n // A closed disclosure (a collapsed navigation group): what it holds appears\n // once it is clicked, which its name alone does not say.\n else if (el.getAttribute('aria-expanded') === 'false') out.push('collapsed');\n if (el.getAttribute('aria-pressed') === 'true') out.push('pressed');\n const current = el.getAttribute('aria-current');\n if (current && current !== 'false') out.push('current');\n return out;\n}\n\n/**\n * Secret fields, by autocomplete: `type=password` alone misses a revealed\n * password, card fields and one-time codes.\n */\nconst SECRET_AUTOCOMPLETE = /^(current-password|new-password|one-time-code|cc-)/;\n\nfunction isSecret(el: Element): boolean {\n if ((el.getAttribute('type') || '').toLowerCase() === 'password') return true;\n // The attribute may carry section/billing tokens before the field name.\n return (el.getAttribute('autocomplete') || '')\n .toLowerCase()\n .split(/\\s+/)\n .some((token) => SECRET_AUTOCOMPLETE.test(token));\n}\n\nfunction value(el: Element, role: string): string | null {\n if (!['textbox', 'searchbox', 'combobox'].includes(role)) return null;\n if (isSecret(el)) return null;\n // `spacedText`, not `textContent`: a contenteditable's hidden descendants\n // are excluded from the walk and its name, and must not return as its value.\n const v = 'value' in el ? String((el as HTMLInputElement).value ?? '') : clean(spacedText(el));\n return v ? v : null;\n}\n\n/**\n * Where an element with an address within the host goes (` -> /path?query`), so a link named for a\n * section (\"Analyses\") still says it leads to the page asked for. Nothing for a\n * link to another site: its address would only be text the host never shows.\n */\nfunction destination(el: Element): string {\n const href = el.getAttribute('href');\n const loc = el.ownerDocument.defaultView?.location;\n if (!href || !loc) return '';\n try {\n const url = new URL(href, loc.href);\n return url.origin === loc.origin ? ` -> ${url.pathname}${url.search}` : '';\n } catch {\n return '';\n }\n}\n\nfunction hidden(el: Element): boolean {\n if (el.classList.contains('filigran-chatbot')) return true;\n // A closed `<details>` renders nothing but its first `<summary>`, though its\n // descendants keep ordinary computed styles.\n const details = el.closest?.('details:not([open])');\n if (details && details !== el) {\n const summary = Array.from(details.children).find((c) => c.tagName.toLowerCase() === 'summary');\n if (!summary?.contains(el)) return true;\n }\n if ((el as HTMLElement).hidden || el.hasAttribute('inert')) return true;\n if (el.getAttribute('aria-hidden') === 'true') return true;\n const style = el.ownerDocument.defaultView?.getComputedStyle(el);\n return style?.display === 'none' || style?.visibility === 'hidden';\n}\n\nexport function takePageSnapshot(doc: Document, maxChars = MAX_SNAPSHOT_CHARS): PageSnapshot {\n const loc = doc.defaultView?.location;\n const url = (loc ? `${loc.pathname}${loc.search}` : '').slice(0, MAX_URL_CHARS);\n const title = clean(doc.title).slice(0, MAX_NAME_CHARS);\n // Without a marker the agent reads a list cut off at the cap as the whole\n // page, so its room is taken out of the budget before anything is listed -\n // as is the header's, which a long title and path could otherwise blow on\n // their own.\n const budget = Math.max(0, maxChars - (TRUNCATION_MARKER.length + 1));\n let truncated = false;\n let header = `# ${title} — ${url}`;\n if (header.length > budget) {\n header = header.slice(0, budget);\n truncated = true;\n }\n const lines = [header];\n let size = header.length;\n const refs = new Map<string, Element>();\n\n const walk = (node: Element): boolean => {\n for (const el of Array.from(node.children)) {\n if (hidden(el)) continue;\n const role = actionableRole(el);\n const level = headingLevel(el);\n let line: string | null = null;\n if (role) {\n const ref = `e${refs.size + 1}`;\n const v = value(el, role);\n const st = states(el);\n line = `[${ref}] ${role} ${quote(accessibleName(el, role))}${st.length ? ` (${st.join(', ')})` : ''}${v ? ` = ${quote(v)}` : ''}${destination(el)}`;\n if (size + line.length + 1 > budget) {\n truncated = true;\n return false;\n }\n refs.set(ref, el);\n } else if (level) {\n line = `heading ${quote(clean(spacedText(el)))} (level ${level})`;\n if (size + line.length + 1 > budget) {\n truncated = true;\n return false;\n }\n }\n if (line) {\n lines.push(line);\n size += line.length + 1;\n }\n // An actionable element's own content is its name, not more elements -\n // unless it wraps controls of its own, which need their own references.\n if ((!role || wrapsControls(el)) && !walk(el)) return false;\n }\n return true;\n };\n if (doc.body && !truncated) walk(doc.body);\n if (truncated) lines.push(TRUNCATION_MARKER);\n\n return { text: lines.join('\\n'), url, title, refs };\n}\n","/**\n * Browser-storage keys of the panel. A host mounting several panels\n * namespaces them with `storageKeyPrefix`; without one the keys are\n * unprefixed.\n */\nexport const STORAGE_KEYS = {\n conversationId: 'filigranChatConversationId',\n legacyChatId: 'filigranChatLegacyChatId',\n agentSlug: 'filigranChatAgentSlug',\n sidebarWidth: 'filigranChatSidebarWidth',\n collapsedWorkspaces: 'filigran-chat-collapsed-workspaces',\n draft: 'filigranChatDraft:',\n draftRefs: 'filigranChatDraftRefs:',\n} as const;\n\nexport function storageKey(prefix: string | undefined, key: string): string {\n return prefix ? `${prefix}:${key}` : key;\n}\n\ntype StorageArea = 'local' | 'session';\n\nfunction area(kind: StorageArea): Storage | null {\n if (typeof window === 'undefined') return null;\n try {\n return kind === 'local' ? window.localStorage : window.sessionStorage;\n } catch {\n return null;\n }\n}\n\n/** Read a key; `null` when storage is unavailable or blocked. */\nexport function readStorage(kind: StorageArea, key: string): string | null {\n try {\n return area(kind)?.getItem(key) ?? null;\n } catch {\n return null;\n }\n}\n\n/** Write a key, or remove it for `null` / `''`; a blocked storage is ignored. */\nexport function writeStorage(kind: StorageArea, key: string, value: string | null): void {\n try {\n const store = area(kind);\n if (!store) return;\n if (value) store.setItem(key, value);\n else store.removeItem(key);\n } catch {\n // Private browsing or a full quota: the panel works without persistence.\n }\n}\n","/**\n * Read the `data:` frames of an SSE body as JSON objects, in order. Comment\n * lines (`: keepalive`), `event:` lines and malformed JSON are skipped.\n * *onEvent* returning `false` stops the read early.\n */\nexport async function readSseEvents(body: ReadableStream<Uint8Array>, onEvent: (evt: Record<string, unknown>) => boolean | void): Promise<void> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n const handle = (rawLine: string): boolean => {\n const line = rawLine.replace(/\\r$/, '');\n if (!line.startsWith('data:')) return true;\n const json = line.startsWith('data: ') ? line.slice(6) : line.slice(5);\n let evt: unknown;\n try {\n evt = JSON.parse(json);\n } catch {\n return true;\n }\n if (!evt || typeof evt !== 'object' || Array.isArray(evt)) return true;\n return onEvent(evt as Record<string, unknown>) !== false;\n };\n try {\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 line of lines) {\n if (!handle(line)) {\n await reader.cancel().catch(() => undefined);\n return;\n }\n }\n }\n buffer += decoder.decode();\n if (buffer) handle(buffer);\n } finally {\n reader.releaseLock();\n }\n}\n","/** A host callback's answer; one that throws is reported and answers nothing, never breaking the turn. */\nexport function askHost<A extends unknown[], R>(callback: ((...args: A) => R) | undefined, ...args: A): R | undefined {\n if (!callback) return undefined;\n try {\n return callback(...args);\n } catch (err) {\n console.error('A chat panel host callback threw', err);\n return undefined;\n }\n}\n","import type { ChatFile } from '../types';\n\n/**\n * Whether *files* hold one that failed to upload. Such a message can neither\n * steer a turn nor wait for one: while a turn holds the conversation (running,\n * winding down, let go of while the page was hidden) it cannot be sent, and\n * once none does it goes without that file.\n */\nexport const hasFailedUpload = (files: readonly ChatFile[]): boolean => files.some((f) => f.uploadStatus === 'error');\n","import type { XtmAgent } from '../types';\nimport { referenceMatches, referenceTokens } from './conversationRefs';\n\n/** An agent picked from the composer's `@` menu: `@key` in the text mentions it. */\nexport interface AgentMention {\n agentId: string;\n name: string;\n key: string;\n}\n\n/** How many agents the `@` menu offers at once, above the conversations. */\nconst AGENT_MENU_LIMIT = 5;\n\n/** What `@` inserts for *agent*: its slug when it reads back as one token, else its name hyphenated. */\nexport function agentMentionKey(agent: Pick<XtmAgent, 'name' | 'slug'>): string | null {\n if (agent.slug && /^[\\w-]+$/.test(agent.slug)) return agent.slug;\n const key = agent.name\n .toLowerCase()\n .replace(/[^\\w-]+/g, '-')\n .replace(/^-+|-+$/g, '');\n return key || null;\n}\n\n/** The agents answering the typed *query*, by key or name, at most `AGENT_MENU_LIMIT`. */\nexport function agentCandidates(agents: readonly XtmAgent[], query: string): AgentMention[] {\n const out: AgentMention[] = [];\n for (const agent of agents) {\n const key = agentMentionKey(agent);\n if (!key || !referenceMatches(query, { key, title: agent.name })) continue;\n out.push({ agentId: agent.id, name: agent.name, key });\n if (out.length >= AGENT_MENU_LIMIT) break;\n }\n return out;\n}\n\n/** *mentions* without those whose `@key` *text* no longer holds; the same array when none was dropped. */\nexport function pruneAgentMentions(mentions: AgentMention[], text: string): AgentMention[] {\n if (mentions.length === 0) return mentions;\n const tokens = new Set(referenceTokens(text));\n const kept = mentions.filter((m) => tokens.has(m.key.toLowerCase()));\n return kept.length === mentions.length ? mentions : kept;\n}\n\n/** *mentions* plus *pick*, unless that agent is already there. */\nexport function withAgentMention(mentions: AgentMention[], pick: AgentMention): AgentMention[] {\n return mentions.some((m) => m.agentId === pick.agentId) ? mentions : [...mentions, pick];\n}\n\n/** The agents a message mentions, in the order their `@key`s appear, each once. */\nexport function mentionedAgentIds(mentions: AgentMention[], text: string): string[] {\n const out: string[] = [];\n for (const token of referenceTokens(text)) {\n for (const m of mentions) {\n if (m.key.toLowerCase() === token && !out.includes(m.agentId)) out.push(m.agentId);\n }\n }\n return out;\n}\n","import { createContext, useContext } from 'react';\n\n/** How the panel's parts reach the backend, and report the end of the user's session. */\nexport interface PanelRequests {\n /** `fetch`, through which a refusal saying the session ended reaches the host (`onSessionEnded`). */\n fetch: typeof fetch;\n /** A frame said the session ended: told to the host once per set of `requestHeaders`. */\n sessionEnded: (reason: string | undefined) => void;\n}\n\n// Outside a panel (a part rendered on its own) requests go out as they are.\nconst PanelRequestsContext = createContext<PanelRequests>({\n fetch: (input, init) => globalThis.fetch(input, init),\n sessionEnded: () => undefined,\n});\n\nexport const PanelRequestsProvider = PanelRequestsContext.Provider;\n\nexport function usePanelRequests(): PanelRequests {\n return useContext(PanelRequestsContext);\n}\n\n/** The panel's `fetch` (see `PanelRequests.fetch`). */\nexport function usePanelFetch(): typeof fetch {\n return useContext(PanelRequestsContext).fetch;\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type {\n AgentStatusState,\n ApiEndpoints,\n ChatSendDecision,\n ChatSendDraft,\n ChatSendOutcome,\n ChatHeldOutcome,\n ChatTurnOutcome,\n BackendType,\n ChatContextUsage,\n ChatConversationRef,\n ChatFile,\n ChatMessage,\n ChatStreamEventInfo,\n ToolApprovalDecision,\n ToolApprovalProposal,\n} from '../types';\nimport type { ParsedAction, ProtocolContext } from './protocols';\nimport { parseAgUiEvent, parseLegacyEvent, parseRestEvent } from './protocols';\nimport { parseToolApprovalProposals, storedMessageId } from './protocols/parseRestEvent';\nimport { createStreamDeltas, type StreamDeltas } from './streamDeltas';\nimport { createSessionGate } from '../utils/sessionGate';\nimport { conversationReferencesUrl, pruneConversationRefs, referencedConversations, withConversationRef } from '../utils/conversationRefs';\nimport { hostTakesPageActions, performPageAction, waitForDomSettle, type PageAction } from '../utils/pageActions';\nimport { takePageSnapshot } from '../utils/pageSnapshot';\nimport { readStorage, STORAGE_KEYS, storageKey, writeStorage } from '../utils/storage';\nimport { readSseEvents } from '../utils/sse';\nimport { randomId } from '../utils/randomId';\nimport { askHost } from '../utils/askHost';\nimport { hasFailedUpload } from '../utils/failedUpload';\nimport { readRefusalDetail, requestHeadersKey } from '../utils/refusal';\nimport { mentionedAgentIds, pruneAgentMentions, withAgentMention, type AgentMention } from '../utils/agentMentions';\nimport { usePanelFetch } from './usePanelRequests';\nimport { optInRoute } from '../utils/routes';\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\n/** Debounce on draft writes so a fast typist doesn't hit storage per keystroke. */\nconst DRAFT_PERSIST_DELAY_MS = 300;\n\nconst draftKey = (prefix: string | undefined, conversationId: string | null): string =>\n storageKey(prefix, `${STORAGE_KEYS.draft}${conversationId ?? 'new'}`);\n\n/**\n * The `@` picks of a draft, kept beside its text: only a pick makes `@key`\n * reference a conversation, so a draft restored without them would go out with\n * its `@key`s as plain text.\n */\nconst draftRefsKey = (prefix: string | undefined, conversationId: string | null): string =>\n storageKey(prefix, `${STORAGE_KEYS.draftRefs}${conversationId ?? 'new'}`);\n\nfunction loadDraftRefs(prefix: string | undefined, conversationId: string | null): ChatConversationRef[] {\n try {\n const parsed: unknown = JSON.parse(readStorage('session', draftRefsKey(prefix, conversationId)) ?? '[]');\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (r): r is ChatConversationRef =>\n !!r && typeof r === 'object' && typeof r.conversationId === 'string' && typeof r.key === 'string' && typeof r.title === 'string',\n );\n } catch {\n return [];\n }\n}\n\nfunction persistDraft(prefix: string | undefined, conversationId: string | null, value: string, refs: ChatConversationRef[]): void {\n writeStorage('session', draftKey(prefix, conversationId), value);\n writeStorage('session', draftRefsKey(prefix, conversationId), refs.length > 0 ? JSON.stringify(refs) : null);\n}\n\n/** A conversation's draft: its text and the `@` picks it still holds. */\nfunction loadDraftState(prefix: string | undefined, conversationId: string | null): { text: string; refs: ChatConversationRef[] } {\n const text = readStorage('session', draftKey(prefix, conversationId)) ?? '';\n return { text, refs: pruneConversationRefs(loadDraftRefs(prefix, conversationId), text) };\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/** How long a dropped turn waits for the event stream to bring it back before reading the history. */\nconst HANDOVER_GRACE_MS = 15000;\n/** How long Stop waits for the backend to end a cancelled turn before letting go of it. */\nconst CANCEL_GRACE_MS = 10000;\n/** Reconnection backoff of the conversation's event stream. */\nconst EVENTS_RETRY_BASE_MS = 1000;\nconst EVENTS_RETRY_MAX_MS = 30000;\n/**\n * How long the page may stay hidden before its event stream is closed. Under\n * HTTP/1.1 a browser holds six connections per origin, and a background tab\n * keeping one open starves the tabs in use; shown again, the stream reopens\n * and the conversation is read back.\n */\nconst EVENTS_HIDDEN_CLOSE_MS = 60000;\n/**\n * How long a relayed turn waits for the event stream's snapshot to replay it,\n * once the stream reopened or the page that let go of it shows again: nothing\n * by then, and it ended while the stream was away (a reopened stream never\n * brings back a turn already over).\n */\nconst EVENTS_RESUME_GRACE_MS = 5000;\n/** How long a relayed turn that ended out of sight waits for the read-back that says how. */\nconst UNSEEN_END_READ_BACK_MS = 10000;\n/** How many turns ended by a provisional end are remembered, awaiting their stored end. */\nconst PROVISIONAL_ENDS_KEPT = 20;\n/**\n * How long sends wait, after a turn's provisional end, for its stored end: the\n * backend still holds the conversation while the turn winds down, and a\n * message sent then would join the ending turn and never be answered.\n */\nconst WIND_DOWN_MS = 5000;\n/** How long an event stream must have lived, with no frame on it, for its next drop to retry from the shortest delay. */\nconst EVENTS_HEALTHY_MS = 5000;\n\n/** The statuses that do not end an agent's wait (`wait_start`). */\nconst STATUSES_DURING_WAIT: ReadonlySet<string> = new Set(['wait_status', 'tool_progress', 'consult_progress']);\n\n/** The actions that belong to a turn, as opposed to the conversation. */\nconst TURN_ACTIONS: ReadonlySet<ParsedAction['action']> = new Set(['status', 'stream', 'done', 'approval_required', 'error', 'cancelled']);\n\n/** The message body's fields the panel owns: a host's extras cannot set them. */\nconst PANEL_FIELDS: ReadonlySet<string> = new Set([\n 'mentioned_agent_ids',\n 'content',\n 'conversation_id',\n 'agent_slug',\n 'supports_tool_approval',\n 'supports_page_actions',\n 'supports_artifacts',\n 'referenced_conversation_ids',\n 'file_ids',\n 'context',\n]);\n\ntype DoneAction = Extract<ParsedAction, { action: 'done' }>;\n\n/** *message* closed by *parsed*: its text, the turn's metadata and its persisted id. */\nfunction withDone(message: ChatMessage, parsed: DoneAction, content: string, completedAt: Date): ChatMessage {\n return {\n ...message,\n content,\n timestamp: completedAt,\n toolNames: parsed.toolNames,\n toolCallCount: parsed.toolCallCount,\n iterations: parsed.iterations,\n attachments: parsed.attachments,\n artifacts: parsed.artifacts,\n reasoning: parsed.reasoning,\n toolCallTrace: parsed.toolCallTrace,\n transferChain: parsed.transferChain,\n isTruncated: parsed.isTruncated,\n agentName: parsed.answeredBy?.agentName ?? message.agentName,\n answeredBy: parsed.answeredBy,\n mentionFailed: parsed.mentionFailed,\n serverId: parsed.messageId ?? message.serverId,\n raw: parsed.raw ?? message.raw,\n };\n}\n\n/** What a stored assistant message adds to the bubble on screen it matches: never its text. */\nconst STORED_ANSWER_FIELDS = [\n 'toolNames',\n 'toolCallCount',\n 'iterations',\n 'attachments',\n 'artifacts',\n 'reasoning',\n 'toolCallTrace',\n 'transferChain',\n 'isTruncated',\n 'agentName',\n 'answeredBy',\n 'mentionFailed',\n 'feedback',\n] as const satisfies readonly (keyof ChatMessage)[];\n\n// The text a user bubble showing other words was sent with, kept on the\n// bubble itself (copies of it keep it) under a key the host never reads.\nconst SENT_TEXT = Symbol('sentText');\ntype SentBubble = ChatMessage & { [SENT_TEXT]?: string };\n\n/** *message*, a user bubble sent with *text*: kept on it when it shows other words. */\nexport function withSentText(message: ChatMessage, text: string): ChatMessage {\n return text === message.content ? message : ({ ...message, [SENT_TEXT]: text } as SentBubble);\n}\n\n/** The text *message* was sent with: what the backend stored, and echoes back. */\nexport function sentTextOf(message: ChatMessage): string {\n return (message as SentBubble)[SENT_TEXT] ?? message.content;\n}\n\n/**\n * How a message on screen (*shownKey*: a user bubble by the text it was sent\n * with) and a stored one (*storedKey*) are told the same without a server id.\n */\nconst shownKey = (m: ChatMessage) => `${m.role}\\u0000${m.role === 'user' ? sentTextOf(m) : m.content}`;\nconst storedKey = (m: ChatMessage) => `${m.role}\\u0000${m.content}`;\n\n/**\n * *shown*, the bubble on screen that *stored* is: it keeps what it shows (the\n * words a user bubble was given, the files sent live) and takes the stored\n * id, and an answer what the store adds to it.\n */\nfunction adoptStored(shown: ChatMessage, stored: ChatMessage): ChatMessage {\n const next: ChatMessage = { ...shown, serverId: stored.serverId ?? shown.serverId, raw: stored.raw ?? shown.raw };\n if (shown.role !== 'assistant') return next;\n const fields = next as unknown as Record<string, unknown>;\n for (const key of STORED_ANSWER_FIELDS) if (stored[key] !== undefined) fields[key] = stored[key];\n return next;\n}\n\n/**\n * *prev* with the conversation read back (*restored*) put into a turn on\n * screen, *turnIds* its bubbles and *liveId* the one it writes to. The stored\n * messages past those shown before the turn take the place of its bubbles\n * they match - by server id, else by role and text, the bubble staying (see\n * `adoptStored`) - and what the turn shows that is not stored, its live\n * bubble among it, follows them. *prev* as it is when nothing stored is\n * missing.\n */\nfunction mergeIntoTurn(prev: ChatMessage[], restored: ChatMessage[], turnIds: ReadonlySet<string>, liveId: string): ChatMessage[] {\n const start = prev.findIndex((m) => turnIds.has(m.id));\n if (start < 0) return prev;\n const before = prev.slice(0, start);\n const region = prev.slice(start);\n // What the history holds past the last message shown before the turn,\n // less what is on screen before it already, unsaved there.\n const shownBefore = new Set(before.map((m) => m.serverId).filter(Boolean));\n let anchor = -1;\n restored.forEach((m, i) => {\n if (m.serverId && shownBefore.has(m.serverId)) anchor = i;\n });\n const unsavedBefore = new Map<string, number>();\n for (const m of before) if (!m.serverId) unsavedBefore.set(shownKey(m), (unsavedBefore.get(shownKey(m)) ?? 0) + 1);\n const missing = restored.slice(anchor + 1).filter((m) => {\n if (m.serverId && shownBefore.has(m.serverId)) return false;\n const pending = unsavedBefore.get(storedKey(m)) ?? 0;\n if (pending === 0) return true;\n unsavedBefore.set(storedKey(m), pending - 1);\n return false;\n });\n if (missing.length === 0) return prev;\n // Each stored message takes the place of the turn's bubble it matches; the\n // live one only by its server id, being still written.\n const taken = new Set<string>();\n const stored = missing.map((m) => {\n const match =\n region.find((r) => !taken.has(r.id) && !!m.serverId && r.serverId === m.serverId) ??\n region.find((r) => !taken.has(r.id) && r.id !== liveId && !r.serverId && shownKey(r) === storedKey(m));\n if (!match) return { ...m, id: storedMessageId(m.serverId) };\n taken.add(match.id);\n return adoptStored(match, m);\n });\n return [...before, ...stored, ...region.filter((m) => !taken.has(m.id))];\n}\n\n/**\n * *prev* with the conversation read back (*restored*) merged in, for a\n * read-back that waited for a turn to end. A stored message already on\n * screen - by server id, else an unsaved one by role and text - stays where\n * it is (see `adoptStored`); one missing goes after the message shown for the\n * stored one before it, in stored order, and before anything shown since\n * that the store does not hold. With none of it on screen (a first\n * read-back), the history goes above what is shown.\n */\nexport function mergeReadBack(prev: ChatMessage[], restored: ChatMessage[]): ChatMessage[] {\n const next = [...prev];\n const byServerId = new Map<string, number>();\n next.forEach((m, i) => {\n if (m.serverId) byServerId.set(m.serverId, i);\n });\n const taken = new Set<number>();\n // Per position on screen (-1: above it all), the stored messages that go after it.\n const inserts = new Map<number, ChatMessage[]>();\n let anchor = -1;\n let changed = false;\n for (const m of restored) {\n let at = m.serverId !== undefined ? byServerId.get(m.serverId) : undefined;\n if (at === undefined) {\n const key = storedKey(m);\n const found = next.findIndex((p, i) => !taken.has(i) && !p.serverId && shownKey(p) === key);\n if (found >= 0) {\n at = found;\n next[found] = adoptStored(next[found], m);\n changed = true;\n }\n }\n if (at !== undefined) {\n taken.add(at);\n // Never back up: a stored message shown out of order keeps the rest after the latest one.\n anchor = Math.max(anchor, at);\n continue;\n }\n const list = inserts.get(anchor) ?? [];\n list.push({ ...m, id: storedMessageId(m.serverId) });\n inserts.set(anchor, list);\n }\n if (inserts.size === 0) return changed ? next : prev;\n const out: ChatMessage[] = [...(inserts.get(-1) ?? [])];\n next.forEach((m, i) => {\n out.push(m, ...(inserts.get(i) ?? []));\n });\n return out;\n}\n\n/** Two texts sent as one. */\nconst joinTexts = (a: string, b: string) => [a, b].filter(Boolean).join('\\n\\n');\n\n/** What the bubble of two sends sent as one shows: `undefined` when neither shows other words than its text. */\nconst joinShown = (a: { text: string; displayText?: string }, b: { text: string; displayText?: string }) =>\n a.displayText !== undefined || b.displayText !== undefined ? joinTexts(a.displayText ?? a.text, b.displayText ?? b.text) : undefined;\n\n/** *b* sent after *a*, as one message. */\nfunction joinHeldParts(a: HeldPart, b: HeldPart): HeldPart {\n const refIds = new Set(a.refs.map((r) => r.conversationId));\n const mentionIds = new Set(a.mentions.map((m) => m.agentId));\n return {\n text: joinTexts(a.text, b.text),\n draft: joinTexts(a.draft, b.draft),\n files: [...a.files, ...b.files],\n refs: [...a.refs, ...b.refs.filter((r) => !refIds.has(r.conversationId))],\n mentions: [...a.mentions, ...b.mentions.filter((m) => !mentionIds.has(m.agentId))],\n extras: a.extras || b.extras ? { ...a.extras, ...b.extras } : undefined,\n displayText: joinShown(a, b),\n };\n}\n\nconst NO_HELD_PART: HeldPart = { text: '', draft: '', files: [], refs: [], mentions: [] };\n\n/** The message *parts* make, sent as one. */\nfunction heldMessageOf(parts: HeldPart[]): HeldMessage {\n return { ...parts.reduce(joinHeldParts, NO_HELD_PART), parts };\n}\n\n/** *part* added to the message already held: one message, sent when the turn ends. */\nfunction mergeHeld(prev: HeldMessage | null, part: HeldPart): HeldMessage {\n return heldMessageOf([...(prev?.parts ?? []), part]);\n}\n\n/** Tell each of *parts* it never went out. */\nfunction tellNotSent(parts: HeldPart[]) {\n for (const part of parts) askHost(part.onNotSent);\n}\n\n/** A send once `onBeforeSend` had its say: what is sent, what the user's bubble shows, the host's fields. */\ninterface DecidedSend {\n text: string;\n displayText: string;\n extras?: Record<string, unknown>;\n}\n\n/**\n * Apply `onBeforeSend`'s *decision* to *text*: `null` when the host took the\n * send over. The bubble shows `displayText` when the host gave one that says\n * something, else *displayFallback* (the handle's `displayText`) when it\n * does, the text sent otherwise.\n */\nexport function applySendDecision(text: string, decision: ChatSendDecision | undefined, displayFallback?: string): DecidedSend | null {\n if (decision === false) return null;\n const adjust = decision && typeof decision === 'object' ? decision : undefined;\n const sent = (typeof adjust?.text === 'string' ? adjust.text : text).trim();\n const shown = typeof adjust?.displayText === 'string' ? adjust.displayText.trim() : '';\n const fallback = typeof displayFallback === 'string' ? displayFallback.trim() : '';\n return { text: sent, displayText: shown || fallback || sent, extras: adjust?.extras };\n}\n\ntype TurnEndAction = Extract<ParsedAction, { action: 'done' | 'error' | 'cancelled' }>;\n\n/** *parsed* when it ends a turn: a last `done`, an `error` or a `cancelled`. */\nfunction turnEndOf(parsed: ParsedAction): TurnEndAction | null {\n return parsed.action === 'error' || parsed.action === 'cancelled' || (parsed.action === 'done' && !parsed.intermediate) ? parsed : null;\n}\n\n/** An end marked `provisional: true`: the turn's stored end follows it. */\nfunction isProvisionalEnd(parsed: ParsedAction): boolean {\n return (parsed.action === 'error' || parsed.action === 'cancelled') && !!parsed.provisional;\n}\n\n/** What `onStreamEvent` is told of a frame, from the panel's own reading of it (*parsed*, none when unreadable). */\nexport function streamEventInfo(relayed: boolean, parsed: ParsedAction | undefined): ChatStreamEventInfo {\n const info: ChatStreamEventInfo = { relayed };\n if (!parsed) return info;\n const end = turnEndOf(parsed);\n if (end && isProvisionalEnd(end)) info.end = 'provisional';\n else if (end?.messageId) info.end = 'stored';\n if (parsed.action === 'status' && parsed.resumed) info.replayed = true;\n return info;\n}\n\n/** The host's extra body fields, read at send time; a throwing getter sends none. */\nfunction readRequestExtras(getter: (() => Record<string, unknown> | undefined) | undefined): Record<string, unknown> {\n const extras = askHost(getter);\n return extras && typeof extras === 'object' ? extras : {};\n}\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 /** The host's in-app navigation, which `act_on_page` uses to open a path (#5559). */\n onRelativeLinkClick?: (href: string) => void;\n t: (key: string) => string;\n maxFileCount?: number;\n maxTotalSize?: number;\n /** Namespace of the browser-storage keys (see `storageKeyPrefix` on the panel). */\n storageKeyPrefix?: string;\n /**\n * The conversation the host holds, when it controls it: `undefined` leaves the\n * panel on its own persisted one. A controlled id is never written to storage.\n */\n controlledConversationId?: string | null;\n /** Called whenever the active conversation changes, whoever changed it. */\n onConversationIdChange?: (id: string | null) => void;\n /** Extra fields merged into each REST message body (see `getRequestExtras` on the panel). */\n getRequestExtras?: () => Record<string, unknown> | undefined;\n /** Every frame of the message stream and the event stream, as received. */\n onStreamEvent?: (event: Record<string, unknown>, info: ChatStreamEventInfo) => void;\n /** A frame of either stream said the user's session ended. */\n onSessionEnded?: (reason: string | undefined) => void;\n /** A conversation was retitled (`conversation_title`). */\n onConversationTitle?: (conversationId: string | null, title: string) => void;\n /**\n * The conversation whose history is on screen: its event stream is followed\n * only then, so a relayed turn is appended to that history.\n */\n historySettledFor?: string | null;\n /** A steer may carry the composer's uploaded files (`file_ids`). */\n steerAttachments?: boolean;\n /** A message that cannot steer the running turn is held, and sent when it ends. */\n holdSends?: boolean;\n /** `@` can mention agents: the message then carries `mentioned_agent_ids`. */\n agentMentionsEnabled?: boolean;\n /** Sees every send first (see `onBeforeSend` on the panel). */\n onBeforeSend?: (draft: ChatSendDraft) => ChatSendDecision;\n /** Told what became of a held message (see `onHeldSendSettled` on the panel). */\n onHeldSendSettled?: (text: string, outcome: ChatHeldOutcome) => void;\n}\n\n/** What a host-driven or held send carries, in place of the composer's content. */\ninterface SendOverride {\n text: string;\n extras?: Record<string, unknown>;\n files?: ChatFile[];\n refs?: ChatConversationRef[];\n mentions?: AgentMention[];\n /** Already seen by `onBeforeSend` (a held message going out). */\n decided?: boolean;\n /** What the user typed for it, as typed (a held message going out): what Stop gives back should it be held again. */\n draft?: string;\n /**\n * What the user's bubble shows in place of `text` (see `ChatSendDecision`):\n * a held message's, or the handle's own, which `onBeforeSend`'s wins over.\n */\n displayText?: string;\n /** Called as the message opens a turn, long before the send itself resolves at the turn's end. */\n onSent?: () => void;\n /**\n * Called when the message, held (the send resolved `'held'`), never goes\n * out: `onBeforeSend` took it over, Stop or a new conversation let go of\n * it, or the send failed.\n */\n onNotSent?: () => void;\n /**\n * Called once the message steered the running turn, with the turn the\n * backend named (`null`: none) and whether the backend said no turn took it\n * (`answered: false`; otherwise a turn it does not name may have).\n */\n onSteered?: (turnId: string | null, unanswered: boolean) => void;\n /** Who sends it, as `onBeforeSend` is told: the host (default) or a voice call. */\n source?: ChatSendDraft['source'];\n}\n\n/** One send held until the running turn ends, as decided. */\ninterface HeldPart {\n text: string;\n /** What the user typed for it, as typed: what Stop gives back. Empty for a host's send. */\n draft: string;\n files: ChatFile[];\n refs: ChatConversationRef[];\n mentions: AgentMention[];\n extras?: Record<string, unknown>;\n /** What the user's bubble shows in place of `text`, when the host said. */\n displayText?: string;\n /**\n * The send as `onBeforeSend` first saw it, while a turn ran: asked again,\n * with `turnRunning: false`, before it leaves as a new turn.\n */\n asked?: { text: string; source: ChatSendDraft['source']; extras?: Record<string, unknown>; displayText?: string };\n /** Told as it goes out, or that it never does (see `SendOverride.onSent`, `onNotSent`). */\n onSent?: () => void;\n onNotSent?: () => void;\n}\n\n/** A message waiting for the running turn to end: its sends (`parts`), joined. */\ninterface HeldMessage extends Omit<HeldPart, 'asked' | 'onSent' | 'onNotSent'> {\n parts: HeldPart[];\n}\n\n/** A relayed turn let go of with the event stream (see `withdrawRelayedTurn`). */\ninterface WithdrawnTurn {\n turnId: string;\n /** The last stored message on screen as it was let go of: what its answer comes after. */\n shownUpTo: string | null;\n /** Its end, out of sight, awaits the read-back past the *from*-th `reloadHistory` (see `endTurnUnseen`). */\n readBack?: { from: number; timer: number };\n}\n\n/** The backend's id of the last message of *messages* it stored, if any. */\nfunction lastStoredId(messages: ChatMessage[]): string | null {\n for (let i = messages.length - 1; i >= 0; i -= 1) {\n const id = messages[i].serverId;\n if (id) return id;\n }\n return null;\n}\n\n/**\n * How a turn that ended out of sight ended, by the conversation read back\n * (*restored*): answered when it holds an answer after *shownUpTo* (the last\n * stored message on screen as the turn was let go of; `null`: from the\n * start), stopped when it holds none, or no longer holds *shownUpTo*.\n */\nexport function unseenTurnOutcome(restored: ChatMessage[], shownUpTo: string | null): ChatTurnOutcome {\n const from = shownUpTo === null ? 0 : restored.findIndex((m) => m.serverId === shownUpTo) + 1;\n if (shownUpTo !== null && from === 0) return 'stopped';\n return restored.slice(from).some((m) => m.role === 'assistant') ? 'answered' : 'stopped';\n}\n\n/** What the pause-recovery route says of a conversation. */\ninterface PendingApprovalsState {\n proposals?: ToolApprovalProposal[];\n turnRunning: boolean;\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 /**\n * A turn runs: the one on screen (`isLoading`), or a relayed turn let go\n * of with the event stream while the page was hidden, until the reopened\n * stream or a read-back says it is over (see `withdrawRelayedTurn`).\n */\n turnRunning: boolean;\n /**\n * No turn on screen, yet sends are held: a turn winding down after its\n * provisional end, or one let go of while the page was hidden.\n */\n sendsHeld: boolean;\n /**\n * How the last turn on screen ended, set as `isLoading` turns false (`null`\n * before any turn ended): what `onTurnRunningChange(false, info)` reports.\n */\n turnEnd: ChatTurnOutcome | null;\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 /** Send the composer's content, or *override*'s without touching the composer. */\n handleSendMessage: (override?: SendOverride) => Promise<ChatSendOutcome>;\n /** A message held until the running turn ends (`holdSends`), if any. */\n heldMessage: HeldMessage | null;\n /** What the backend said when it refused a steer or a Stop, shown by the composer; `null` when nothing. */\n notice: string | null;\n cancelHeldMessage: () => void;\n /** Read the conversation back from the server again. */\n reloadHistory: () => void;\n /**\n * Put the conversation read back (*restored*) into the relayed turn it was\n * read back for, which runs on: true when one was (see `handleEventFrame`),\n * false when the read-back is the panel's to apply.\n */\n mergeTurnHistory: (restored: ChatMessage[]) => boolean;\n /**\n * The conversation read back for the *readFor*-th read-back asked for\n * (`historyReloadNonce`): *restored*, or `null` when it could not be read.\n */\n settleUnseenEnd: (readFor: number, restored: ChatMessage[] | null) => void;\n /** The agents picked from the `@` menu whose `@key` the draft still holds. */\n agentMentions: AgentMention[];\n addAgentMention: (mention: AgentMention) => void;\n /** End an agent's wait early (\"Continue now\"); `null` when the host offers no route. */\n skipWait: (() => Promise<void>) | null;\n /** Read what a conversation is paused on and whether its turn runs; `null` when the host offers no route. */\n readPendingApprovals: ((convId: string) => Promise<PendingApprovalsState | null>) | null;\n /**\n * Start a fresh conversation; with *workspaceId* (a string), the session is\n * created inside that workspace (`workspace_id` on the session `POST`).\n */\n handleNewChat: (workspaceId?: string) => void;\n /** The workspace the next conversation will be created in, if any. */\n pendingWorkspaceId: string | null;\n /** Forget *workspaceId* as the next conversation's workspace (it was deleted). */\n clearPendingWorkspace: (workspaceId: string) => void;\n handleStopGenerating: () => void;\n /**\n * The conversations picked from the composer's `@` menu whose `@key` the\n * draft still holds; sent as `referenced_conversation_ids` with the next\n * message.\n */\n conversationRefs: ChatConversationRef[];\n /** Record a pick from the `@` menu. */\n addConversationRef: (ref: ChatConversationRef) => 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\nexport function 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 /** See {@link ApiEndpoints.conversationReferences}. */\n referencedConversationIds?: string[];\n /** Same promise, for `act_on_page` frames (#5559): answered on the approve path. */\n supportsPageActions?: boolean;\n /** The host's own fields; never overriding the panel's. */\n extras?: 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 ?? randomId(),\n runId: randomId(),\n messages: [{ id: randomId(), role: 'user', content }],\n tools: [],\n context: [],\n state: {},\n forwardedProps: opts.agentSlug ? { agentSlug: opts.agentSlug } : {},\n };\n default: {\n // The host's fields never stand in for the panel's own, set or not.\n const hostFields = Object.fromEntries(Object.entries(opts.extras ?? {}).filter(([key]) => !PANEL_FIELDS.has(key)));\n const body: Record<string, unknown> = { ...hostFields, 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 // Only when the message holds a pick: a host that never named the route\n // sends exactly the body it always did.\n if (opts.referencedConversationIds && opts.referencedConversationIds.length > 0) {\n body.referenced_conversation_ids = opts.referencedConversationIds;\n }\n if (opts.supportsPageActions) {\n body.supports_page_actions = true;\n }\n // The panel renders the deck, report and video markers itself whatever\n // the host proxies: the cards need no route to show and link to XTM One.\n body.supports_artifacts = true;\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 onRelativeLinkClick,\n t,\n maxFileCount = DEFAULT_MAX_FILE_COUNT,\n maxTotalSize = DEFAULT_MAX_TOTAL_SIZE,\n storageKeyPrefix,\n controlledConversationId,\n onConversationIdChange,\n getRequestExtras,\n onStreamEvent,\n onSessionEnded,\n onConversationTitle,\n historySettledFor = null,\n steerAttachments = false,\n holdSends = false,\n agentMentionsEnabled = false,\n onBeforeSend,\n onHeldSendSettled,\n}: UseChatOptions): UseChatReturn {\n const panelFetch = usePanelFetch();\n const isLegacy = backendType === 'legacy';\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [turnEnd, setTurnEnd] = useState<ChatTurnOutcome | null>(null);\n const [agentStatus, setAgentStatus] = useState<AgentStatusState | null>(null);\n const isControlled = controlledConversationId !== undefined;\n const conversationStorageKey = storageKey(storageKeyPrefix, STORAGE_KEYS.conversationId);\n const legacyChatIdStorageKey = storageKey(storageKeyPrefix, STORAGE_KEYS.legacyChatId);\n const [conversationId, setConversationId] = useState<string | null>(() =>\n isControlled ? (controlledConversationId ?? null) : readStorage('local', conversationStorageKey),\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 - and the\n // conversations it references.\n const [initialDraft] = useState(() => loadDraftState(storageKeyPrefix, conversationId));\n const [inputValue, setInputValue] = useState(initialDraft.text);\n const [conversationRefs, setConversationRefs] = useState<ChatConversationRef[]>(initialDraft.refs);\n const [agentMentions, setAgentMentions] = useState<AgentMention[]>([]);\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>(() => readStorage('local', legacyChatIdStorageKey));\n\n const historyLoadedRef = useRef(false);\n const abortControllerRef = useRef<AbortController | null>(null);\n // Page actions abort on their own: closing the panel must stop them touching\n // the host page without cancelling the turn, whose answer a host expects to\n // find in history when the panel reopens.\n const pageActionControllerRef = useRef<AbortController | null>(null);\n // The running stream's pending deltas, so stopping it can apply them and a\n // new chat can drop them.\n const streamDeltasRef = useRef<StreamDeltas | null>(null);\n // The turn this panel streams itself, so the event stream never renders it\n // again, and the one of another origin it relays, if any.\n const ownTurnActiveRef = useRef(false);\n const ownTurnIdRef = useRef<string | null>(null);\n const relayedTurnRef = useRef<{ turnId: string; renderer: ReturnType<typeof createTurnRenderer> } | null>(null);\n // A Stop sent to the backend, and the backstop that lets go of the turn if\n // it never reports the end.\n const cancelTimerRef = useRef<number | null>(null);\n // The renderer of the turn on screen, this panel's own or a relayed one:\n // what Stop marks.\n const liveRendererRef = useRef<ReturnType<typeof createTurnRenderer> | null>(null);\n // A relayed turn stopped here: its later frames are not a new turn.\n const stoppedTurnIdRef = useRef<string | null>(null);\n // Stop was pressed: what was held goes back to the composer instead of out.\n const stopRequestedRef = useRef(false);\n // A message waiting for the running turn to end (`holdSends`).\n const [heldMessage, setHeldMessage] = useState<HeldMessage | null>(null);\n const heldMessageRef = useRef(heldMessage);\n heldMessageRef.current = heldMessage;\n // A refused steer's or Stop's reason, until the next send or the turn's end.\n const [notice, setNotice] = useState<string | null>(null);\n // The notice says what becomes of the next send: it outlives the turn's end.\n const noticeUntilSendRef = useRef(false);\n // Relayed turns ended by a provisional end, by turn id, with the answer each\n // left on screen: their stored end only gives that answer its id.\n const provisionalEndsRef = useRef(new Map<string, string>());\n // A turn ended provisionally whose stored end is still to come (see\n // `WIND_DOWN_MS`): sends are held meanwhile. *relayed*: the event stream's.\n const windDownRef = useRef<{ timer: number; relayed: boolean } | null>(null);\n const [windingDown, setWindingDown] = useState(false);\n // The panel's own stream read on past a provisional end, for its stored end:\n // aborted with the chat it belongs to.\n const storedEndControllerRef = useRef<AbortController | null>(null);\n // The panel's own turn has started on the backend: its stream delivered a\n // frame (a backend answers a send once its turn holds the conversation).\n const ownTurnStartedRef = useRef(false);\n // A cancel sent before the own turn started: its answer is awaited\n // ('asked'), or was `turn: \"none\"`, which says nothing of that turn, and the\n // cancel goes again once the turn starts ('resend').\n const cancelBeforeStartRef = useRef<'off' | 'asked' | 'resend'>('off');\n // Frames applied to the turn on screen so far, and what a Stop found at its\n // click: a refused Stop restores the status only if no frame came since.\n const turnFrameSeqRef = useRef(0);\n const stopContextRef = useRef<{ statusBefore: AgentStatusState | null; approvalsBefore: ToolApprovalProposal[] | null; frameSeq: number } | null>(\n null,\n );\n // A relayed turn on screen whose event stream reopened: it waits for the\n // stream's snapshot to replay it, and is over when none does.\n const resumeWaitRef = useRef<{ turn: { turnId: string }; timer: number } | null>(null);\n // The renderer of a relayed turn the reopened event stream replayed, whose\n // conversation is read back for the segments it closed meanwhile.\n const turnReadBackRef = useRef<ReturnType<typeof createTurnRenderer> | null>(null);\n // A relayed turn let go of with the event stream (see `withdrawRelayedTurn`),\n // whose replay reads the conversation back: until the reopened stream says\n // whether it runs on, or a read-back how it ended (`endTurnUnseen`), sends\n // wait and the host is not told it ended.\n const withdrawnTurnRef = useRef<WithdrawnTurn | null>(null);\n const [turnWithdrawn, setTurnWithdrawn] = useState(false);\n const setWithdrawnTurn = (turn: WithdrawnTurn | null) => {\n const readBack = withdrawnTurnRef.current?.readBack;\n if (readBack && turn !== withdrawnTurnRef.current) window.clearTimeout(readBack.timer);\n withdrawnTurnRef.current = turn;\n setTurnWithdrawn(turn !== null);\n };\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n // Read-backs asked for so far (`reloadHistory`): `historyReloadNonce`.\n const historyReloadsRef = useRef(0);\n // This panel's identity on the conversation's event stream: the backend\n // never relays to it the turns it started itself.\n const [clientId, setClientId] = useState(() => randomId());\n // The panel's own turn whose stream dropped, now awaited from the event stream.\n const handoverTimerRef = useRef<number | null>(null);\n const handoverTurnIdRef = useRef<string | null>(null);\n const clearHandover = () => {\n if (handoverTimerRef.current !== null) window.clearTimeout(handoverTimerRef.current);\n handoverTimerRef.current = null;\n handoverTurnIdRef.current = null;\n };\n /** A turn ended provisionally: sends wait for its stored end, a while at most. */\n const startWindDown = (relayed: boolean) => {\n if (windDownRef.current) window.clearTimeout(windDownRef.current.timer);\n const token = { timer: 0, relayed };\n token.timer = window.setTimeout(() => endWindDown(token), WIND_DOWN_MS);\n windDownRef.current = token;\n setWindingDown(true);\n return token;\n };\n /** Sends go out again: *token*'s wind-down (any, without) is over. */\n const endWindDown = (token?: { timer: number; relayed: boolean }) => {\n const current = windDownRef.current;\n if (!current || (token && token !== current)) return;\n window.clearTimeout(current.timer);\n windDownRef.current = null;\n setWindingDown(false);\n };\n /** A relayed turn's wind-down is over: its stored end came, or another turn holds the conversation. */\n const endRelayedWindDown = () => {\n if (windDownRef.current?.relayed) endWindDown();\n };\n\n const getRequestExtrasRef = useRef(getRequestExtras);\n getRequestExtrasRef.current = getRequestExtras;\n const onStreamEventRef = useRef(onStreamEvent);\n onStreamEventRef.current = onStreamEvent;\n const onSessionEndedRef = useRef(onSessionEnded);\n onSessionEndedRef.current = onSessionEnded;\n const onConversationTitleRef = useRef(onConversationTitle);\n onConversationTitleRef.current = onConversationTitle;\n const onBeforeSendRef = useRef(onBeforeSend);\n onBeforeSendRef.current = onBeforeSend;\n const onHeldSendSettledRef = useRef(onHeldSendSettled);\n onHeldSendSettledRef.current = onHeldSendSettled;\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 /**\n * The turn on screen is over, *ended* so: the one place a turn stops\n * running, which says how it ended along with it (a panel already idle\n * keeps the end it last told).\n */\n const stopRunning = (ended: ChatTurnOutcome) => {\n if (isLoadingRef.current) setTurnEnd(ended);\n isLoadingRef.current = false;\n setIsLoading(false);\n if (!noticeUntilSendRef.current) setNotice(null);\n };\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 // The elements of the last host-page snapshot, by the reference the next\n // `act_on_page` action names them with (#5559).\n const pageRefsRef = useRef<Map<string, Element>>(new Map());\n const onRelativeLinkClickRef = useRef(onRelativeLinkClick);\n onRelativeLinkClickRef.current = onRelativeLinkClick;\n // A page action answers from the closure of the render that began the turn,\n // so a header rotated since would go out stale and 401 the answer.\n const requestHeadersRef = useRef(requestHeaders);\n requestHeadersRef.current = requestHeaders;\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 // One session creation at a time, for the chat on screen: a new chat\n // abandons the one in flight (see `createSessionGate`).\n const [sessionGate] = useState(createSessionGate);\n // The workspace the next session is created in (\"new conversation\" inside\n // a workspace group). A ref for the request, state for the list to show it.\n const pendingWorkspaceRef = useRef<string | null>(null);\n const [pendingWorkspaceId, setPendingWorkspaceId] = useState<string | 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 // A pick counts while its `@key` is in the text: editing it out drops it.\n useEffect(() => {\n setConversationRefs((prev) => pruneConversationRefs(prev, inputValue));\n setAgentMentions((prev) => pruneAgentMentions(prev, inputValue));\n }, [inputValue]);\n const addAgentMention = useCallback((mention: AgentMention) => {\n setAgentMentions((prev) => withAgentMention(prev, mention));\n }, []);\n\n const addConversationRef = useCallback((ref: ChatConversationRef) => {\n setConversationRefs((prev) => withConversationRef(prev, ref));\n }, []);\n\n /**\n * Put what a message carried back in the composer, *before* what was typed\n * since or after it; a pick the draft holds already is not added twice.\n */\n const returnToComposer = (\n { draft, files, refs, mentions }: Pick<HeldMessage, 'draft' | 'files' | 'refs' | 'mentions'>,\n { before }: { before: boolean },\n ) => {\n if (draft) setInputValue((prev) => (!prev ? draft : before ? `${draft}\\n${prev}` : `${prev}\\n\\n${draft}`));\n if (files.length > 0) setAttachedFiles((prev) => (before ? [...files, ...prev] : [...prev, ...files]));\n if (refs.length > 0) {\n setConversationRefs((prev) => {\n const added = refs.filter((r) => !prev.some((p) => p.conversationId === r.conversationId));\n return before ? [...added, ...prev] : [...prev, ...added];\n });\n }\n if (mentions.length > 0) {\n setAgentMentions((prev) => {\n const added = mentions.filter((m) => !prev.some((p) => p.agentId === m.agentId));\n return before ? [...added, ...prev] : [...prev, ...added];\n });\n }\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 => optInRoute(apiBaseUrl, apiEndpoints, backendType, 'approve');\n // Same reason as `requestHeadersRef`: the answer must resolve the route\n // against the endpoints as they are now, not as they were at turn start.\n const getApproveUrlRef = useRef(getApproveUrl);\n getApproveUrlRef.current = getApproveUrl;\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<PendingApprovalsState | null> => {\n const url = getPendingApprovalsUrl(convId);\n if (!url) return null;\n try {\n const res = await panelFetch(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 historyReloadsRef.current += 1;\n setHistoryReloadNonce(historyReloadsRef.current);\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 const getCancelUrl = () => optInRoute(apiBaseUrl, apiEndpoints, backendType, 'cancel');\n const getSkipWaitUrl = () => optInRoute(apiBaseUrl, apiEndpoints, backendType, 'skipWait');\n const eventsBase = optInRoute(apiBaseUrl, apiEndpoints, backendType, 'events');\n const getEventsUrl = (convId: string): string | null =>\n eventsBase ? `${eventsBase}/${encodeURIComponent(convId)}/events?client_id=${encodeURIComponent(clientId)}` : null;\n const hasEvents = !!eventsBase;\n /**\n * The pause-recovery URL for one conversation, or null when the host has\n * not exposed the route. See {@link ApiEndpoints.pendingApprovals}.\n */\n const pendingApprovalsBase = optInRoute(apiBaseUrl, apiEndpoints, backendType, 'pendingApprovals');\n const getPendingApprovalsUrl = (convId: string): string | null =>\n pendingApprovalsBase ? `${pendingApprovalsBase}/${encodeURIComponent(convId)}/pending-approvals` : null;\n /** Names this panel to a backend that relays turns, so its own are not relayed back. */\n const clientIdHeader = (): Record<string, string> => (hasEvents ? { 'X-Chat-Client-Id': clientId } : {});\n /** POST to a route about the conversation's running turn: its id, and *fields* beside it. */\n const postConversation = (url: string, convId: string, fields?: Record<string, unknown>) =>\n panelFetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeadersRef.current ?? {}), ...clientIdHeader() },\n body: JSON.stringify({ conversation_id: convId, ...fields }),\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 onConversationIdChangeRef = useRef(onConversationIdChange);\n onConversationIdChangeRef.current = onConversationIdChange;\n const updateConversationId = useCallback(\n (id: string | null, { notify = true }: { notify?: boolean } = {}) => {\n const changed = conversationIdRef.current !== id;\n conversationIdRef.current = id;\n setConversationId(id);\n if (!isControlled) writeStorage('local', conversationStorageKey, id);\n if (changed && notify) onConversationIdChangeRef.current?.(id);\n },\n [isControlled, conversationStorageKey],\n );\n\n /**\n * Ensure a conversation exists. Concurrent callers of the same chat (e.g.\n * multiple files selected at once) share a single session creation, and a\n * creation answered after a new chat was started resolves to `null` without\n * touching the chat that replaced it.\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 (sessionGate.pending) return sessionGate.pending;\n\n const sessionsUrl = getSessionsUrl();\n if (!sessionsUrl) return null;\n\n return sessionGate.run(async (isCurrent, signal) => {\n const workspaceId = pendingWorkspaceRef.current;\n const res = await panelFetch(sessionsUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify(workspaceId ? { agent_slug: slug, workspace_id: workspaceId } : { agent_slug: slug }),\n signal,\n });\n if (!res.ok || !isCurrent()) return null;\n const data = await res.json();\n // The user started another chat meanwhile: this conversation is not\n // its, and neither is the workspace or the history guard to reset.\n if (!isCurrent()) return null;\n const convId = (data?.conversation_id as string) ?? null;\n if (convId) {\n // Just created: there is no history to restore. Left armed, the\n // panel's restore would fetch it while the first turn streams and\n // replace the answer being written with the one message persisted.\n historyLoadedRef.current = true;\n updateConversationId(convId);\n pendingWorkspaceRef.current = null;\n setPendingWorkspaceId(null);\n }\n return convId;\n });\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 panelFetch(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: randomId(),\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 what the user typed (*draft*) is restored into the\n * composer — prepended on its own line if the user already typed something\n * new — unless the host sent it (`null`), or a 409 holds it, so the message\n * is never silently lost and never resets the in-flight run state.\n */\n const steerMessage = async (\n content: string,\n files: ChatFile[],\n draft: string | null,\n shown = content,\n ): Promise<{ result: 'ok'; turnId: string | null; unanswered: boolean } | { result: 'ended' } | { result: 'failed'; detail?: string | null }> => {\n const steerUrl = getSteerUrl();\n const convId = conversationIdRef.current;\n if (!steerUrl || !convId) return { result: 'failed' };\n\n const optimistic = withSentText(\n { id: randomId(), role: 'user', content: shown, timestamp: new Date(), files: files.length > 0 ? files : undefined },\n content,\n );\n setMessages((prev) => [...prev, optimistic]);\n const fileIds = files.filter((f) => f.uploadStatus === 'done' && f.fileId).map((f) => f.fileId!);\n // Why the backend refused it, when it says.\n let detail: string | null = null;\n\n try {\n const res = await panelFetch(steerUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}), ...clientIdHeader() },\n body: JSON.stringify({ conversation_id: convId, content, agent_slug: agentSlug, ...(fileIds.length > 0 ? { file_ids: fileIds } : {}) }),\n });\n if (res.ok) {\n // Stored in the thread, whatever the body says (`stored`: handed to\n // no turn the backend can name). The turn it joined, when it says;\n // `answered: false`, no turn took it, and the next one reads it.\n const data: unknown = await res.json().catch(() => null);\n const body = data && typeof data === 'object' ? (data as { turn_id?: unknown; answered?: unknown }) : {};\n const turnId = body.turn_id;\n return { result: 'ok', turnId: typeof turnId === 'string' && turnId ? turnId : null, unanswered: body.answered === false };\n }\n setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));\n // Nothing running any more: the turn ended while this was on its way.\n // Held where the host allows it; otherwise the text goes back to the\n // composer, with no notice for an ordinary race.\n if (res.status === 409 && holdSends) return { result: 'ended' };\n if (res.status !== 409) detail = await readRefusalDetail(res);\n } catch {\n setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));\n }\n // A message the user typed goes back where it came from; one the host sent does not land in the composer.\n if (draft !== null) returnToComposer({ draft, files, refs: [], mentions: [] }, { before: true });\n return { result: 'failed', detail };\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 panelFetch(approveUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}), ...clientIdHeader() },\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 && !relayedTurnRef.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 ? t('This turn is no longer waiting for a decision.') : 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 /**\n * Perform one `act_on_page` action on the host page and answer it with a\n * fresh snapshot (#5559). The answer goes to the approve path, which the host\n * proxies already forward; the turn waits for it on the open stream.\n *\n * A 409 means the turn no longer waits for this action (it timed out or\n * ended): nothing to retry, so it is dropped.\n */\n const answerPageAction = async (actionId: string, action: PageAction, conversationIdHint?: string) => {\n const approveUrl = getApproveUrlRef.current();\n const convId = conversationIdHint ?? conversationIdRef.current;\n if (!approveUrl || !convId || typeof document === 'undefined') return;\n // The turn's own signal: Stop, New chat and switching conversation abort it,\n // and this runs detached from the stream, so without it a stopped turn would\n // still click the page and then answer for a conversation that is gone.\n const signal = pageActionControllerRef.current?.signal;\n // The stream outlives a closed panel, so the turn is still waiting: it is\n // answered without the page and without the aborted signal, which would\n // cancel the request too and leave it waiting out its timeout per step.\n const refuse = async (error: string) => {\n try {\n await panelFetch(approveUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeadersRef.current ?? {}), ...clientIdHeader() },\n body: JSON.stringify({\n conversation_id: convId,\n page_action: { action_id: actionId, ok: false, error, url: '', title: '', snapshot: '' },\n }),\n });\n } catch {\n // The turn's own page wait ends it; there is nothing to show here.\n }\n };\n if (signal?.aborted) return refuse('panel_closed');\n // The same opt-in that decides whether the capability is advertised, tested\n // again on the way in: an unsolicited frame must not read or act on the page\n // of a host that never offered it. Answered, so the turn does not hang.\n if (!hostTakesPageActions(approveUrl, onRelativeLinkClickRef.current)) {\n return refuse('page_actions_not_supported');\n }\n setAgentStatus((prev) => (prev ? { ...prev, status: 'acting_on_page' } : { status: 'acting_on_page' }));\n try {\n const result = await performPageAction(action, pageRefsRef.current, onRelativeLinkClickRef.current, document, signal);\n if (signal?.aborted) return refuse('panel_closed');\n if (action.type !== 'snapshot') await waitForDomSettle(document, { signal });\n if (signal?.aborted) return refuse('panel_closed');\n // The snapshot can throw on its own (a very deep DOM, a host\n // `getComputedStyle` that raises). It rides in `error` while `ok` stays\n // the action's own result: `ok: false` would have the agent retry a click\n // that already ran, and a field of its own would be dropped unseen by a\n // backend that does not know it, leaving an empty page and no sign why.\n let page = { text: '', url: '', title: '' };\n let snapshotError: string | null = null;\n try {\n const snapshot = takePageSnapshot(document);\n pageRefsRef.current = snapshot.refs;\n page = { text: snapshot.text, url: snapshot.url, title: snapshot.title };\n } catch (err) {\n pageRefsRef.current = new Map();\n snapshotError = `${err instanceof Error ? err.message : String(err)}`.slice(0, 500);\n }\n await panelFetch(approveUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeadersRef.current ?? {}), ...clientIdHeader() },\n signal,\n body: JSON.stringify({\n conversation_id: convId,\n page_action: {\n action_id: actionId,\n ok: result.ok,\n error: result.error ?? (snapshotError === null ? null : `snapshot_failed: ${snapshotError}`),\n url: page.url,\n title: page.title,\n snapshot: page.text,\n },\n }),\n });\n } catch {\n // The turn's own page wait ends it; there is nothing to show here.\n } finally {\n // Not for a turn that was stopped: its replacement owns the status line.\n if (!signal?.aborted) {\n setAgentStatus((prev) => (prev?.status === 'acting_on_page' ? { ...prev, status: 'thinking' } : prev));\n }\n }\n };\n\n /**\n * Render one turn's frames into the transcript: the turn this panel started,\n * read off its message stream, or one of another origin (a second tab, a\n * reload) relayed by the conversation's event stream. A segment closed by an\n * intermediate `done` is followed by a fresh assistant bubble.\n */\n const createTurnRenderer = (firstAssistantId: string, relayed: boolean) => {\n let currentAssistantId = firstAssistantId;\n // Every bubble the turn opened: a steered turn answers in several.\n const segmentIds = [firstAssistantId];\n let accumulated = '';\n let doneReceived = false;\n // The turn's last `done` came, with nothing written after it.\n let answered = false;\n // An error, a stop or the session's end the stream reported: what it wrote must not be put back over it.\n let ended: 'error' | 'cancelled' | 'session' | null = null;\n const ctx: ProtocolContext = { hasUsedTools: false, activeNodeId: '' };\n\n const deltas = createStreamDeltas((textChanged, thinking) => {\n if (textChanged) {\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 }\n if (thinking) {\n setAgentStatus((prev) => ({\n ...prev,\n status: prev?.status ?? 'thinking',\n thinkingContent: (prev?.thinkingContent ?? '') + thinking,\n }));\n }\n });\n\n // Events after a `done` open a new segment: a steered turn answers the\n // steering message in a follow-up pass on the same stream, and each\n // segment carries its own reasoning.\n const ensureSegment = () => {\n if (!doneReceived) return;\n // Nothing of the closed segment may land in the new one.\n deltas.flush();\n doneReceived = false;\n answered = false;\n accumulated = '';\n currentAssistantId = randomId();\n segmentIds.push(currentAssistantId);\n const segmentId = currentAssistantId;\n setMessages((prev) => [...prev, { id: segmentId, role: 'assistant', content: '', timestamp: new Date() }]);\n setAgentStatus({ status: 'thinking' });\n };\n\n const applyStatus = (parsed: Extract<ParsedAction, { action: 'status' }>) => {\n ensureSegment();\n const segId = currentAssistantId;\n // Orthogonal to the status label below: a progress frame can carry a\n // fresh context reading whatever phase it announces.\n if (parsed.contextUsage) setContextUsage(parsed.contextUsage);\n if (parsed.status === 'thinking_text') {\n deltas.thinking(parsed.thinkingContent ?? '');\n return;\n }\n // Every other status lands after the text and reasoning that arrived\n // before it.\n deltas.flush();\n const elapsedStartMs = typeof parsed.elapsedS === 'number' ? Date.now() - parsed.elapsedS * 1000 : undefined;\n if (parsed.resumed) {\n // A re-attached turn is replayed from its start: what it had written\n // so far arrives again, whole.\n accumulated = '';\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: '' } : m)));\n setAgentStatus({ status: 'thinking', elapsedS: parsed.elapsedS, elapsedStartMs });\n } else if (parsed.status === 'stream_retract') {\n // Rare: text that streamed as a provisional answer turned out to\n // precede tool calls — discard the answer bubble (the text re-arrives\n // as thinking_text right after, so it lands in the reasoning window).\n accumulated = '';\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: '' } : m)));\n setAgentStatus((prev) => ({ status: 'analyzing', thinkingContent: prev?.thinkingContent }));\n } else if (parsed.status === 'tool_heartbeat') {\n // Liveness signal during a long tool execution: update the elapsed\n // counter but KEEP the current status label/tools, re-anchoring the\n // local 1s ticker in ChatThinking on the server clock.\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 if (parsed.status === 'wait_start') {\n setAgentStatus((prev) => ({\n status: 'wait_start',\n thinkingContent: prev?.thinkingContent,\n waitSeconds: parsed.waitSeconds,\n waitReason: parsed.waitReason,\n waitStartMs: Date.now(),\n }));\n } else {\n // What a wait reports while it lasts (what it watches, a running\n // tool's progress) leaves it on screen, with its countdown and\n // \"Continue now\"; any other status ends it, as streamed text does.\n setAgentStatus((prev) =>\n prev?.status === 'wait_start' && STATUSES_DURING_WAIT.has(parsed.status)\n ? prev\n : { status: parsed.status, tools: parsed.tools, thinkingContent: prev?.thinkingContent },\n );\n }\n };\n\n const applyDone = (parsed: Extract<ParsedAction, { action: 'done' }>) => {\n deltas.flush();\n doneReceived = true;\n if (!parsed.intermediate) answered = true;\n // A completed segment cannot still be waiting on a decision, so any\n // prompt left standing (a submission that failed against a turn somebody\n // else already answered) is stale.\n setPendingApprovals(null);\n setApprovalError(null);\n applyDoneEffects(parsed, relayed);\n const segId = currentAssistantId;\n const finalContent = parsed.content || accumulated;\n // The backend stores a segment when its `done` is sent, and a restore\n // shows that time: stamping the send time instead would change the\n // footer on the next reload.\n const completedAt = new Date();\n landAnswer(segId, parsed.messageId, (m) => withDone(m, parsed, finalContent, completedAt));\n };\n\n /** Apply one frame; `'end'` when it closes the turn. */\n const apply = (parsed: ParsedAction): 'continue' | 'end' => {\n turnFrameSeqRef.current += 1;\n switch (parsed.action) {\n case 'status':\n applyStatus(parsed);\n return 'continue';\n case 'stream':\n ensureSegment();\n accumulated += parsed.content;\n deltas.text();\n return 'continue';\n case 'done':\n applyDone(parsed);\n // The stream of a turn this panel started ends by itself; a relayed\n // turn ends on its last segment.\n return relayed && !parsed.intermediate ? 'end' : 'continue';\n case 'approval_required':\n // The turn stops here and the stream goes silent until a decision is\n // POSTed back. Not an end state: the rest of the turn arrives on this\n // same stream afterwards, so the reasoning that led to the proposed\n // call stays on screen for the reviewer to judge it by.\n deltas.flush();\n approvalConversationIdRef.current = parsed.conversationId ?? conversationIdRef.current;\n approvalDetachedRef.current = false;\n setApprovalError(null);\n setPendingApprovals(parsed.proposals);\n setAgentStatus((prev) => (prev ? { ...prev, status: 'awaiting_approval' } : { status: 'awaiting_approval' }));\n return 'continue';\n case 'error': {\n ensureSegment();\n deltas.flush();\n const segId = currentAssistantId;\n // The backend reported a failure of its own, so the generic apology\n // rather than \"check the configuration\": the connection plainly worked.\n ended = 'error';\n setMessages((prev) =>\n prev.map((m) => (m.id === segId ? { ...m, content: parsed.content || t('Sorry, an error occurred. Please try again.') } : m)),\n );\n return 'end';\n }\n case 'cancelled': {\n deltas.flush();\n ended = 'cancelled';\n const segId = currentAssistantId;\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, stopped: true } : m)));\n return 'end';\n }\n case 'session_ended': {\n // The user is signed out: what was written stays, an empty bubble goes.\n deltas.flush();\n ended = 'session';\n const segId = currentAssistantId;\n setMessages((prev) => prev.filter((m) => !(m.id === segId && !m.content)));\n return 'end';\n }\n default:\n return 'continue';\n }\n };\n\n return {\n ctx,\n deltas,\n apply,\n currentAssistantId: () => currentAssistantId,\n accumulated: () => accumulated,\n doneReceived: () => doneReceived,\n segmentIds: () => segmentIds,\n ended: () => ended,\n /** How the turn ended, by its frames: an `error` failed it, a stop or the session's end (even after the last `done`) or no last `done` stopped it. */\n outcome: (): ChatTurnOutcome => (ended === 'error' ? 'failed' : ended === null && answered ? 'answered' : 'stopped'),\n };\n };\n\n /** What a `done` changes beyond its answer: the conversation's id (own turns), the context gauge, a transfer. */\n const applyDoneEffects = (parsed: DoneAction, relayed: boolean) => {\n if (parsed.conversationId && !relayed) updateConversationId(parsed.conversationId);\n // Closing reading wins: a turn whose last iteration compacted ends lower\n // 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 };\n\n /**\n * Give the answer bubble *bubbleId* what its turn's end says of it\n * (*land*) - unless the answer *messageId* names is on screen already (read\n * back meanwhile): the bubble then goes, so it is not shown twice.\n */\n const landAnswer = (bubbleId: string, messageId: string | undefined, land: (message: ChatMessage) => ChatMessage) => {\n setMessages((prev) => {\n if (messageId && prev.some((m) => m.id !== bubbleId && m.serverId === messageId)) return prev.filter((m) => m.id !== bubbleId);\n return prev.map((m) => (m.id === bubbleId ? land(m) : m));\n });\n };\n\n /**\n * The stored end of a turn a provisional end already ended (a `done`,\n * `cancelled` or `error` carrying `message_id`): it gives the answer that\n * turn left, *bubbleId*, its persisted id - and a `done` all it carries, as a\n * live one would - and starts nothing. False when *parsed* is not that end.\n */\n const applyStoredEnd = (bubbleId: string, parsed: ParsedAction, relayed: boolean): boolean => {\n const end = turnEndOf(parsed);\n const messageId = end?.messageId;\n if (!end || !messageId) return false;\n // The context gauge and a transfer are the running turn's to say, when one runs since.\n if (end.action === 'done' && !isLoadingRef.current) applyDoneEffects(end, relayed);\n const completedAt = new Date();\n landAnswer(bubbleId, messageId, (m) =>\n end.action === 'done' ? withDone(m, end, end.content || m.content, completedAt) : { ...m, serverId: messageId, raw: end.raw ?? m.raw },\n );\n return true;\n };\n\n /**\n * One frame of either stream, as the panel reads it, told to the host with\n * what was read in it; `undefined` (told all the same) when it cannot be read.\n */\n const readFrame = (\n parse: (evt: Record<string, unknown>, ctx: ProtocolContext) => ParsedAction,\n evt: Record<string, unknown>,\n ctx: ProtocolContext,\n relayed: boolean,\n ): ParsedAction | undefined => {\n let parsed: ParsedAction | undefined;\n try {\n parsed = parse(evt, ctx);\n } catch {\n parsed = undefined;\n }\n askHost(onStreamEventRef.current, evt, streamEventInfo(relayed, parsed));\n if (parsed?.action === 'session_ended') askHost(onSessionEndedRef.current, parsed.reason);\n return parsed;\n };\n\n /** Frames about the conversation rather than a turn; true when handled. */\n const applyConversationFrame = (parsed: ParsedAction): boolean => {\n switch (parsed.action) {\n case 'user_message': {\n const relayedMessage = parsed.message;\n setMessages((prev) => {\n if (relayedMessage.serverId && prev.some((m) => m.serverId === relayedMessage.serverId)) return prev;\n // This tab's own message, echoed back: it takes the persisted id rather than a second bubble.\n // Looked for among the latest messages only: an older one with the same words is another message.\n let own = prev.length - 1;\n while (own >= 0 && !(prev[own].role === 'user' && !prev[own].serverId && sentTextOf(prev[own]) === relayedMessage.content)) own -= 1;\n if (own >= 0 && prev.length - own <= 3) {\n return prev.map((m, i) => (i === own ? { ...m, serverId: relayedMessage.serverId, raw: relayedMessage.raw } : m));\n }\n return [...prev, relayedMessage];\n });\n return true;\n }\n case 'conversation_title':\n onConversationTitleRef.current?.(parsed.conversationId ?? conversationIdRef.current, parsed.title);\n return true;\n case 'approval_resolved':\n setPendingApprovals(null);\n setApprovalError(null);\n return true;\n default:\n return false;\n }\n };\n\n /**\n * The turn on screen ended and its end was seen, *outcome* so: nothing of it\n * waits any more (a Stop, the status, a decision), and the recovery probe\n * has nothing to find - on a fresh conversation too, whose id only just\n * arrived.\n */\n const settleTurn = (outcome: ChatTurnOutcome) => {\n clearCancelRequest();\n stopRunning(outcome);\n setAgentStatus(null);\n // Nothing can carry a decision any more: a prompt left up would offer an\n // answer to a question nobody is listening for.\n setPendingApprovals(null);\n setApprovalError(null);\n approvalConversationIdRef.current = null;\n approvalDetachedRef.current = false;\n probedConversationRef.current = conversationIdRef.current;\n };\n\n const handleSendMessage = async (override?: SendOverride): Promise<ChatSendOutcome> => {\n // A host-driven or held send leaves the composer, its attachments and picks alone.\n const fromComposer = !override;\n const typed = override ? override.text : inputValue;\n const draft = fromComposer ? inputValue : (override?.draft ?? '');\n const files = fromComposer ? attachedFiles : (override?.files ?? []);\n if (!typed.trim() && files.length === 0) return 'dropped';\n const source = fromComposer ? 'composer' : (override?.source ?? 'host');\n // A turn ended provisionally and still holds the conversation (see\n // `WIND_DOWN_MS`), or one let go of with the event stream may run on (see\n // `withdrawRelayedTurn`): what is sent now waits for its end.\n const windingDownNow = !isLoadingRef.current && (windDownRef.current !== null || withdrawnTurnRef.current !== null);\n const turnRunning = isLoadingRef.current || withdrawnTurnRef.current !== null;\n // The send as the host's hook sees it while a turn runs, kept on a\n // message then held: asked again before it leaves as a new turn.\n const asked =\n !override?.decided && turnRunning && onBeforeSendRef.current\n ? { text: typed, source, extras: override?.extras, displayText: override?.displayText }\n : undefined;\n // The host's say before anything leaves: it may take the send over, or reword it.\n const decided = applySendDecision(\n typed,\n override?.decided ? undefined : askHost(onBeforeSendRef.current, { text: typed, source, turnRunning }),\n override?.displayText,\n );\n if (!decided) return 'dropped';\n noticeUntilSendRef.current = false;\n setNotice(null);\n const hookExtras = decided.extras;\n // The fields this message adds to the body: the handle's, then the hook's.\n const sendExtras = override?.extras || hookExtras ? { ...override?.extras, ...hookExtras } : undefined;\n const text = decided.text;\n // What the user's bubble shows: the host's words for it, else the text sent.\n const shownText = override?.decided ? override.displayText?.trim() || text : decided.displayText;\n // Kept on a held message only when it differs, so a plain one merges without displayText.\n const heldDisplay = shownText !== text ? shownText : undefined;\n // The picks the text still holds, when the host still offers the feature.\n const sentRefs = !fromComposer\n ? (override?.refs ?? [])\n : conversationReferencesUrl(apiBaseUrl, apiEndpoints, backendType)\n ? referencedConversations(conversationRefs, inputValue)\n : [];\n // The agents the text still mentions, in mention order.\n const sentMentions = !fromComposer ? (override?.mentions ?? []) : agentMentionsEnabled ? agentMentions : [];\n const mentionedIds = mentionedAgentIds(sentMentions, text);\n if (!text && files.length === 0) return 'dropped';\n const clearComposer = () => {\n if (!fromComposer) return;\n setInputValue('');\n setConversationRefs([]);\n setAgentMentions([]);\n setAttachedFiles([]);\n };\n if (isLoadingRef.current || windingDownNow) {\n // A file that failed to upload can neither steer nor wait: the composer\n // offers no send then (only the composer attaches files).\n if (hasFailedUpload(files)) return 'dropped';\n // Mid-run steering. References, mentions and the host's own fields never\n // steer: a steer reaches the loop as text alone, and the agent would\n // never read them. Attachments steer only where the host says the steer\n // route takes them, once uploaded.\n const uploaded = files.every((f) => f.uploadStatus === 'done');\n const canSteerThis =\n !windingDownNow &&\n !!getSteerUrl() &&\n !!conversationIdRef.current &&\n sentRefs.length === 0 &&\n mentionedIds.length === 0 &&\n Object.keys(sendExtras ?? {}).length === 0 &&\n (files.length === 0 || (steerAttachments && uploaded));\n if (canSteerThis) {\n clearComposer();\n const steered = await steerMessage(text, files, fromComposer ? draft : null, shownText);\n if (steered.result === 'ended') {\n setHeldMessage((prev) =>\n mergeHeld(prev, {\n text,\n draft,\n files,\n refs: [],\n mentions: [],\n extras: sendExtras,\n displayText: heldDisplay,\n asked,\n onSent: override?.onSent,\n onNotSent: override?.onNotSent,\n }),\n );\n return 'held';\n }\n if (steered.result === 'ok') {\n if (steered.unanswered) {\n noticeUntilSendRef.current = true;\n setNotice(t('This message will be read with your next one.'));\n }\n override?.onSteered?.(steered.turnId, steered.unanswered);\n return 'steered';\n }\n if (steered.detail) setNotice(steered.detail);\n return 'failed';\n }\n // Otherwise it waits for the turn to end, where the host allows it -\n // and always for a turn winding down, which is over on screen.\n if ((holdSends || windingDownNow) && uploaded) {\n clearComposer();\n setHeldMessage((prev) =>\n mergeHeld(prev, {\n text,\n draft,\n files,\n refs: sentRefs,\n mentions: sentMentions,\n extras: sendExtras,\n displayText: heldDisplay,\n asked,\n onSent: override?.onSent,\n onNotSent: override?.onNotSent,\n }),\n );\n return 'held';\n }\n return 'dropped';\n }\n\n const userMsg = withSentText(\n {\n id: randomId(),\n role: 'user',\n content: shownText,\n timestamp: new Date(),\n files: files.length > 0 ? [...files] : undefined,\n conversationRefs: sentRefs.length > 0 ? sentRefs : undefined,\n },\n text,\n );\n const assistantId = randomId();\n setMessages((prev) => [...prev, userMsg, { id: assistantId, role: 'assistant', content: '', timestamp: new Date() }]);\n clearComposer();\n isLoadingRef.current = true;\n setIsLoading(true);\n setAgentStatus({ status: 'thinking' });\n // The chat this turn belongs to: a new chat (or a switch) moves the gate\n // on, and the turn's end must then leave the replacing chat's state alone.\n const turnGeneration = sessionGate.generation;\n\n const turn = createTurnRenderer(assistantId, false);\n streamDeltasRef.current = turn.deltas;\n liveRendererRef.current = turn;\n stopRequestedRef.current = false;\n ownTurnActiveRef.current = true;\n ownTurnIdRef.current = null;\n ownTurnStartedRef.current = false;\n // Set when the turn joined is already on screen from the event stream,\n // which then carries it to its end.\n let handedOver = false;\n // The backend took the message: its stream answered.\n let accepted = false;\n // The send never reached a stream: refused, or the backend unreachable.\n let failed = false;\n // The stream said the turn is over: a last `done`, an error or a stop.\n let sawTurnEnd = false;\n // The turn ended provisionally (see `cancelled`): it is over on screen,\n // and the stream is read on only for its stored end.\n let awaitingStoredEnd = false;\n // The turn's state was let go of (see `release`).\n let released = false;\n // Sends held while the stored end is awaited (see `startWindDown`).\n let windDown: ReturnType<typeof startWindDown> | null = null;\n /**\n * The stream is gone but the turn runs on, on the backend: the event stream,\n * under a new name for this panel, replays it. False when it cannot.\n */\n const handOver = (): boolean => {\n if (!hasEvents || !conversationIdRef.current || !ownTurnIdRef.current || turn.ended()) return false;\n handedOver = true;\n // The replay brings back the segment being written, never those an\n // intermediate `done` closed: they stay.\n if (!turn.doneReceived()) {\n const segId = turn.currentAssistantId();\n setMessages((prev) => prev.filter((m) => m.id !== segId));\n }\n handoverTurnIdRef.current = ownTurnIdRef.current;\n ownTurnIdRef.current = null;\n setClientId(randomId());\n if (handoverTimerRef.current !== null) window.clearTimeout(handoverTimerRef.current);\n handoverTimerRef.current = window.setTimeout(() => {\n handoverTimerRef.current = null;\n if (relayedTurnRef.current || handoverTurnIdRef.current === null) return;\n // Nothing came: the turn is over, and its answer is in the history,\n // if it got one; nothing on screen says it did.\n handoverTurnIdRef.current = null;\n stopRunning('stopped');\n setAgentStatus(null);\n reloadHistory();\n }, HANDOVER_GRACE_MS);\n return true;\n };\n\n const controller = new AbortController();\n pageActionControllerRef.current?.abort();\n const pageActions = new AbortController();\n /** Let go of the turn's state: once, when it ends - which may be before its stream does. */\n const release = () => {\n if (released) return;\n released = true;\n turn.deltas.close();\n if (streamDeltasRef.current === turn.deltas) streamDeltasRef.current = null;\n if (liveRendererRef.current === turn) liveRendererRef.current = null;\n ownTurnActiveRef.current = false;\n // The turn is over, so its page actions are too - but only this turn's:\n // a replacement already owns the refs by then.\n if (pageActionControllerRef.current === pageActions) pageActions.abort();\n if (abortControllerRef.current === controller) abortControllerRef.current = null;\n // An abandoned turn's chat is gone: `resetChat` reset the shared\n // state already, and resetting it again here would cut the Stop button,\n // the status and the approvals of a turn the new chat started since.\n // A turn handed over to the event stream is still running.\n if (sessionGate.generation === turnGeneration && !handedOver) settleTurn(failed ? 'failed' : turn.outcome());\n };\n try {\n abortControllerRef.current = controller;\n pageActionControllerRef.current = pageActions;\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 // A conversation started inside a workspace is created first, filed\n // there: the message endpoint would create it in no workspace. Should\n // that fail (the workspace was archived or is no longer shared), the\n // message still goes, in a conversation of no workspace, rather than\n // being lost. So is one whose turns outlive their stream: Stop,\n // \"Continue now\" and steering name the conversation from the first turn.\n const turnsOutliveStream = !!(getCancelUrl() || getSkipWaitUrl() || hasEvents);\n if (!conversationIdRef.current && (pendingWorkspaceRef.current || turnsOutliveStream)) {\n const generation = sessionGate.generation;\n const created = await ensureConversation(agentSlug);\n // A new chat replaced this one meanwhile: the send went with it, and\n // the workspace now pending is the new chat's, not this send's.\n if (sessionGate.generation !== generation) return 'dropped';\n if (!created) {\n pendingWorkspaceRef.current = null;\n setPendingWorkspaceId(null);\n }\n }\n\n // Use conversationIdRef to get the latest value (may have been set by eager upload)\n const requestBody = buildRequestBody(backendType, text, {\n legacyChatId,\n conversationId: conversationIdRef.current,\n agentSlug,\n pageContext: pageContextRef.current,\n supportsToolApproval: getApproveUrl() !== null,\n referencedConversationIds: sentRefs.map((r) => r.conversationId),\n supportsPageActions: hostTakesPageActions(getApproveUrl(), onRelativeLinkClickRef.current),\n extras: { ...readRequestExtras(getRequestExtrasRef.current), ...(sendExtras ?? {}) },\n });\n if (fileIds.length > 0) {\n (requestBody as Record<string, unknown>).file_ids = fileIds;\n }\n if (mentionedIds.length > 0) {\n (requestBody as Record<string, unknown>).mentioned_agent_ids = mentionedIds;\n }\n\n const res = await panelFetch(getMessagesUrl(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}), ...clientIdHeader() },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n });\n\n if (!res.ok || !res.body) {\n // A refusal before the stream (a rate limit, a refused option, an\n // unknown conversation) says why in its `detail`: that is what shows.\n const detail = res.ok ? null : await readRefusalDetail(res);\n failed = true;\n setMessages((prev) =>\n prev.map((m) => (m.id === assistantId ? { ...m, content: detail ?? t('Unable to connect. Please check the configuration.') } : m)),\n );\n return 'failed';\n }\n accepted = true;\n override?.onSent?.();\n\n const parseEvent = getParser(backendType);\n await readSseEvents(res.body, (evt) => {\n const parsed = readFrame(parseEvent, evt, turn.ctx, false);\n if (!parsed) return true;\n try {\n if (awaitingStoredEnd) {\n // A new chat took the panel over: nothing of this turn is on screen.\n if (sessionGate.generation !== turnGeneration) return false;\n if (applyConversationFrame(parsed)) return true;\n return !applyStoredEnd(turn.currentAssistantId(), parsed, false);\n }\n if (!ownTurnStartedRef.current && liveRendererRef.current === turn) {\n // The turn holds the conversation now: a cancel answered `none`\n // before it did goes again, to reach it (one not answered yet\n // goes again on its answer).\n ownTurnStartedRef.current = true;\n if (cancelBeforeStartRef.current === 'resend') {\n cancelBeforeStartRef.current = 'off';\n if (cancelTimerRef.current !== null) postCancel(cancelTimerRef.current);\n }\n }\n if (!ownTurnIdRef.current && typeof evt.turn_id === 'string' && evt.turn_id) ownTurnIdRef.current = evt.turn_id;\n if (parsed.action === 'folded') {\n if (parsed.turnId) ownTurnIdRef.current = parsed.turnId;\n return true;\n }\n if (applyConversationFrame(parsed)) return true;\n if (parsed.action === 'set_chat_id') {\n setLegacyChatId(parsed.chatId);\n writeStorage('local', legacyChatIdStorageKey, parsed.chatId);\n return true;\n }\n if (parsed.action === 'page_action_required') {\n turn.deltas.flush();\n // Not awaited: the stream stays silent until the answer lands,\n // and the reader must keep draining its keep-alives meanwhile.\n void answerPageAction(parsed.actionId, parsed.pageAction, parsed.conversationId);\n return true;\n }\n if (parsed.action === 'session_ended' || turnEndOf(parsed)) sawTurnEnd = true;\n if (turn.apply(parsed) !== 'end') return true;\n if (isProvisionalEnd(parsed)) {\n // Over for the user and the host now; its stored end, still to\n // come on this stream, carries the answer's id.\n turn.deltas.flush();\n awaitingStoredEnd = true;\n // A turn that outlives its stream holds the conversation until\n // that end: sends wait for it.\n if (turnsOutliveStream) windDown = startWindDown(false);\n\n storedEndControllerRef.current = controller;\n release();\n return true;\n }\n return false;\n } catch {\n // A frame that could not be applied is skipped, not fatal.\n return true;\n }\n });\n turn.deltas.flush();\n // Ended without saying the turn is over (a proxy's idle timeout, a\n // server restart): the turn may well run on.\n if (!handedOver && !sawTurnEnd && handOver()) return 'sent';\n if (!handedOver && !turn.ended() && turn.accumulated() && !turn.doneReceived()) {\n const segId = turn.currentAssistantId();\n const text = turn.accumulated();\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: text || t('No response.') } : m)));\n }\n } catch (err) {\n // A frame still pending would land after the error below, or in the\n // chat that replaced an aborted stream.\n turn.deltas.close();\n // Only the stored end was still awaited: the turn itself is over.\n if (released) return 'sent';\n if (err instanceof DOMException && err.name === 'AbortError') return accepted ? 'sent' : 'dropped';\n const segId = turn.currentAssistantId();\n // The stream dropped mid-turn, but the backend runs the turn on.\n if (handOver()) return 'sent';\n if (!accepted) failed = true;\n setMessages((prev) => prev.map((m) => (m.id === segId ? { ...m, content: t('Sorry, an error occurred. Please try again.') } : m)));\n } finally {\n release();\n if (windDown) endWindDown(windDown);\n if (storedEndControllerRef.current === controller) storedEndControllerRef.current = null;\n }\n return accepted ? 'sent' : 'failed';\n };\n\n /** End the relayed turn on screen, if any, as its own stream would. */\n const endRelayedTurn = () => {\n const relayedTurn = relayedTurnRef.current;\n if (!relayedTurn) return;\n relayedTurnRef.current = null;\n relayedTurn.renderer.deltas.flush();\n relayedTurn.renderer.deltas.close();\n if (streamDeltasRef.current === relayedTurn.renderer.deltas) streamDeltasRef.current = null;\n if (liveRendererRef.current === relayedTurn.renderer) liveRendererRef.current = null;\n const segId = relayedTurn.renderer.currentAssistantId();\n setMessages((prev) => prev.filter((m) => !(m.id === segId && !m.content && !m.stopped)));\n settleTurn(relayedTurn.renderer.outcome());\n };\n\n const endRelayedTurnRef = useRef(endRelayedTurn);\n endRelayedTurnRef.current = endRelayedTurn;\n\n /**\n * Let go of the relayed turn on screen, the event stream closed: reopened,\n * it replays the segment being written, which goes; those an intermediate\n * `done` closed stay, and the replay reads back the ones closed meanwhile.\n * The turn may run on: it is still running for sends and for the host (see\n * `withdrawnTurnRef`) until the reopened stream replays it or shows it over.\n */\n const withdrawRelayedTurn = () => {\n const relayedTurn = relayedTurnRef.current;\n if (!relayedTurn) return;\n const unfinished = relayedTurn.renderer.doneReceived() ? null : relayedTurn.renderer.currentAssistantId();\n endRelayedTurn();\n setWithdrawnTurn({ turnId: relayedTurn.turnId, shownUpTo: lastStoredId(messagesRef.current) });\n if (unfinished) setMessages((prev) => prev.filter((m) => m.id !== unfinished));\n };\n const withdrawRelayedTurnRef = useRef(withdrawRelayedTurn);\n withdrawRelayedTurnRef.current = withdrawRelayedTurn;\n\n /** The relayed turn let go of with the event stream is over, *ended* so when its end said. */\n const endWithdrawnTurn = (ended?: ChatTurnOutcome) => {\n if (!withdrawnTurnRef.current) return;\n setWithdrawnTurn(null);\n if (ended) setTurnEnd(ended);\n };\n const endWithdrawnTurnRef = useRef(endWithdrawnTurn);\n endWithdrawnTurnRef.current = endWithdrawnTurn;\n\n const clearResumeWait = () => {\n if (resumeWaitRef.current) window.clearTimeout(resumeWaitRef.current.timer);\n resumeWaitRef.current = null;\n };\n\n /**\n * The relayed turn on screen, or the one let go of, ended out of sight:\n * nothing replayed it, or the event stream cannot reopen. It still runs for\n * sends and the host until the conversation read back says how it ended\n * (`settleUnseenEnd`), {@link UNSEEN_END_READ_BACK_MS} at most; a replay\n * meanwhile carries it on.\n */\n const endTurnUnseen = () => {\n clearResumeWait();\n const relayedTurn = relayedTurnRef.current;\n if (relayedTurn) {\n // What it wrote stays until the read-back brings what it stored.\n const shownUpTo = lastStoredId(messagesRef.current);\n endRelayedTurn();\n setWithdrawnTurn({ turnId: relayedTurn.turnId, shownUpTo });\n }\n const withdrawn = withdrawnTurnRef.current;\n if (!withdrawn || withdrawn.readBack) return;\n if (!getSessionsUrl()) {\n endWithdrawnTurn('stopped');\n return;\n }\n reloadHistory();\n const timer = window.setTimeout(() => {\n if (withdrawnTurnRef.current === withdrawn) endWithdrawnTurn('stopped');\n }, UNSEEN_END_READ_BACK_MS);\n withdrawn.readBack = { from: historyReloadsRef.current, timer };\n };\n const endTurnUnseenRef = useRef(endTurnUnseen);\n endTurnUnseenRef.current = endTurnUnseen;\n\n /**\n * The conversation read back for the *readFor*-th `reloadHistory` (*restored*;\n * `null`: it could not be read): a turn that ended out of sight ends, as\n * the read-back says (`unseenTurnOutcome`).\n */\n const settleUnseenEnd = (readFor: number, restored: ChatMessage[] | null) => {\n const withdrawn = withdrawnTurnRef.current;\n if (!withdrawn?.readBack || readFor < withdrawn.readBack.from) return;\n endWithdrawnTurn(restored ? unseenTurnOutcome(restored, withdrawn.shownUpTo) : 'stopped');\n };\n\n /**\n * The event stream reopened (it ends every so often, or dropped), or the\n * page that let go of a relayed turn shows again: that turn is still running\n * only if the stream's snapshot replays it, which it does at once. Otherwise\n * it ended while the stream was away, and its end is only in the history.\n * A turn let go of waits from the page showing again, however often the\n * stream reopens since.\n */\n const awaitRelayedResume = () => {\n const relayedTurn = relayedTurnRef.current;\n const withdrawn = withdrawnTurnRef.current;\n if (!relayedTurn && withdrawn && (withdrawn.readBack || resumeWaitRef.current?.turn === withdrawn)) return;\n clearResumeWait();\n const awaited = relayedTurn ?? withdrawn;\n if (!awaited) return;\n const timer = window.setTimeout(() => {\n resumeWaitRef.current = null;\n if (relayedTurn ? relayedTurnRef.current !== relayedTurn : withdrawnTurnRef.current !== withdrawn) return;\n endTurnUnseen();\n }, EVENTS_RESUME_GRACE_MS);\n resumeWaitRef.current = { turn: awaited, timer };\n };\n const awaitRelayedResumeRef = useRef(awaitRelayedResume);\n awaitRelayedResumeRef.current = awaitRelayedResume;\n\n const mergeTurnHistory = (restored: ChatMessage[]): boolean => {\n const renderer = turnReadBackRef.current;\n turnReadBackRef.current = null;\n // The panel's own turn started since: the read-back waits for it, as any.\n if (!renderer || ownTurnActiveRef.current) return false;\n const turnIds = new Set(renderer.segmentIds());\n const liveId = renderer.currentAssistantId();\n setMessages((prev) => mergeIntoTurn(prev, restored, turnIds, liveId));\n return true;\n };\n\n // The frame types the event stream sent without `turn_id`, said once each.\n const unnamedTurnFramesRef = useRef(new Set<string>());\n const warnUnnamedTurnFrame = (type: string) => {\n if (unnamedTurnFramesRef.current.has(type)) return;\n unnamedTurnFramesRef.current.add(type);\n console.warn(\n `A \"${type}\" frame of the event stream names no turn and is not read: the events route needs \\`turn_id\\` on every turn frame (see \"Turns that outlive their stream\" in the README)`,\n );\n };\n /** One frame of the conversation's event stream. */\n const handleEventFrame = (evt: Record<string, unknown>) => {\n let relayedTurn = relayedTurnRef.current;\n const parsed = readFrame(parseRestEvent, evt, relayedTurn?.renderer.ctx ?? { hasUsedTools: false, activeNodeId: '' }, true);\n if (!parsed) return;\n if (applyConversationFrame(parsed)) return;\n if (parsed.action === 'session_ended') {\n // Signed out: no turn of another origin goes on for this user, and a\n // handed-over turn of our own will not be replayed.\n endRelayedTurn();\n endWithdrawnTurn('stopped');\n if (handoverTurnIdRef.current !== null && !ownTurnActiveRef.current) {\n clearHandover();\n stopRunning('stopped');\n setAgentStatus(null);\n }\n return;\n }\n if (!TURN_ACTIONS.has(parsed.action)) return;\n // Every turn frame of the event stream names its turn: one naming none is not read.\n const turnId = typeof evt.turn_id === 'string' && evt.turn_id ? evt.turn_id : null;\n if (!turnId) {\n warnUnnamedTurnFrame(String(evt.type));\n return;\n }\n // A turn a provisional end already ended (never the panel's own turn):\n // its stored end gives the answer it left its id, and nothing of it\n // reopens the turn.\n const provisionalBubble = provisionalEndsRef.current.get(turnId);\n if (provisionalBubble !== undefined) {\n if (applyStoredEnd(provisionalBubble, parsed, true)) {\n provisionalEndsRef.current.delete(turnId);\n endRelayedWindDown();\n }\n return;\n }\n // The turn this panel streams itself is never rendered twice.\n if (ownTurnActiveRef.current || turnId === ownTurnIdRef.current) return;\n if (turnId === stoppedTurnIdRef.current) return;\n const awaited = resumeWaitRef.current;\n if (awaited) {\n // The first turn frame since the stream reopened says whether the turn\n // on screen still runs.\n clearResumeWait();\n if (relayedTurn && relayedTurn === awaited.turn && turnId !== relayedTurn.turnId) {\n // Another turn runs: this one ended while the stream was away, and\n // the read-back brings its stored answer in place of the one it was\n // writing.\n const unfinished = relayedTurn.renderer.doneReceived() ? null : relayedTurn.renderer.currentAssistantId();\n endRelayedTurn();\n if (unfinished) setMessages((prev) => prev.filter((m) => m.id !== unfinished));\n reloadHistory();\n relayedTurn = null;\n } else if (relayedTurn && relayedTurn === awaited.turn) {\n // Otherwise it is replayed (`thinking.resumed`), having run on all\n // along: the snapshot brings back the segment being written, which\n // the ack resets, never those an intermediate `done` closed. Those\n // closed while the stream was away are only in the history, read\n // back into the turn (`mergeTurnHistory`).\n turnReadBackRef.current = relayedTurn.renderer;\n reloadHistory();\n }\n }\n\n let current = relayedTurn;\n if (current && current.turnId !== turnId) {\n endRelayedTurn();\n current = null;\n }\n if (!current) {\n // A turn let go of with the event stream goes on here, or another runs:\n // it ended while the stream was away, and its answer is in the history.\n const withdrawn = withdrawnTurnRef.current;\n const replayed = withdrawn?.turnId === turnId;\n // The end of a turn never seen begins nothing.\n if ((parsed.action === 'cancelled' || parsed.action === 'error') && !replayed) {\n if (withdrawn) endTurnUnseen();\n return;\n }\n if (withdrawn && !replayed) {\n endWithdrawnTurn();\n reloadHistory();\n }\n const bubbleId = randomId();\n setMessages((prev) => [...prev, { id: bubbleId, role: 'assistant', content: '', timestamp: new Date() }]);\n current = { turnId, renderer: createTurnRenderer(bubbleId, true) };\n relayedTurnRef.current = current;\n // Another turn holds the conversation: one winding down is gone.\n endRelayedWindDown();\n\n streamDeltasRef.current = current.renderer.deltas;\n liveRendererRef.current = current.renderer;\n // A turn this panel lost sight of (its own stream dropped, the event\n // stream closed while the page was hidden), replayed: the segments it\n // closed meanwhile are only in the history, read back into it.\n setWithdrawnTurn(null);\n const lostSight = turnId === handoverTurnIdRef.current || replayed;\n if (evt.resumed === true && lostSight) {\n turnReadBackRef.current = current.renderer;\n reloadHistory();\n }\n clearHandover();\n stopRequestedRef.current = false;\n isLoadingRef.current = true;\n setIsLoading(true);\n setAgentStatus({ status: 'thinking' });\n }\n if (current.renderer.apply(parsed) === 'end') {\n if (isProvisionalEnd(parsed)) {\n // Its stored end is still to come, and the backend holds the\n // conversation until then: sends wait for it.\n const ends = provisionalEndsRef.current;\n ends.set(current.turnId, current.renderer.currentAssistantId());\n while (ends.size > PROVISIONAL_ENDS_KEPT) ends.delete(ends.keys().next().value!);\n startWindDown(true);\n }\n endRelayedTurn();\n }\n };\n const handleEventFrameRef = useRef(handleEventFrame);\n handleEventFrameRef.current = handleEventFrame;\n\n const handleNewChat = (workspaceId?: string) => resetChat(workspaceId, true);\n\n /** Let go of the message held, if any: the host hears it goes nowhere. */\n const cancelHeld = () => {\n const held = heldMessageRef.current;\n if (held) {\n onHeldSendSettledRef.current?.(held.text, 'cancelled');\n tellNotSent(held.parts);\n }\n setHeldMessage(null);\n };\n\n /** The new-chat reset; *notify*: the host hears of the empty conversation (not on a switch, which names the next one). */\n const resetChat = (workspaceId: string | undefined, notify: boolean) => {\n // A string only: the header and the sidebar pass this as a click handler,\n // and the click event must never reach the session request.\n const workspace = typeof workspaceId === 'string' && workspaceId ? workspaceId : null;\n pendingWorkspaceRef.current = workspace;\n setPendingWorkspaceId(workspace);\n // Nothing of the abandoned stream may land in the fresh chat.\n streamDeltasRef.current?.close();\n streamDeltasRef.current = null;\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n pageActionControllerRef.current?.abort();\n pageActionControllerRef.current = null;\n // Cancel any in-flight file uploads\n uploadAbortRef.current.abort();\n uploadAbortRef.current = new AbortController();\n // A session still being created was the abandoned chat's.\n sessionGate.abandon();\n relayedTurnRef.current?.renderer.deltas.close();\n relayedTurnRef.current = null;\n clearResumeWait();\n turnReadBackRef.current = null;\n setWithdrawnTurn(null);\n provisionalEndsRef.current.clear();\n // The abandoned chat's own stream, read on for a stored end, goes too.\n storedEndControllerRef.current?.abort();\n storedEndControllerRef.current = null;\n endWindDown();\n ownTurnActiveRef.current = false;\n cancelHeld();\n clearCancelRequest();\n clearHandover();\n noticeUntilSendRef.current = false;\n stopRunning('stopped');\n setMessages([]);\n setInputValue('');\n setConversationRefs([]);\n setAgentMentions([]);\n setAttachedFiles([]);\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 historyLoadedRef.current = false;\n if (isLegacy) {\n setLegacyChatId(null);\n writeStorage('local', legacyChatIdStorageKey, null);\n } else {\n updateConversationId(null, { notify });\n }\n };\n\n const clearPendingWorkspace = (workspaceId: string) => {\n if (pendingWorkspaceRef.current !== workspaceId) return;\n pendingWorkspaceRef.current = null;\n setPendingWorkspaceId(null);\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. The host hears of\n // the conversation switched to alone, never of an empty one between.\n resetChat(undefined, false);\n if (!isLegacy) {\n updateConversationId(id);\n // `resetChat` blanked the composer; bring back this conversation's\n // own unsent draft, if any, with its references.\n const draft = loadDraftState(storageKeyPrefix, id);\n setInputValue(draft.text);\n setConversationRefs(draft.refs);\n }\n };\n\n const clearCancelRequest = () => {\n if (cancelTimerRef.current !== null) window.clearTimeout(cancelTimerRef.current);\n cancelTimerRef.current = null;\n cancelBeforeStartRef.current = 'off';\n stopContextRef.current = null;\n };\n\n /**\n * Let go of the turn on screen without the backend's word that it ended.\n * *running*: a turn was on screen, whose answer then says it was stopped.\n */\n const stopLocally = (running = true) => {\n clearCancelRequest();\n const live = liveRendererRef.current;\n liveRendererRef.current = null;\n const relayedTurn = relayedTurnRef.current;\n const stoppedTurnId = relayedTurn?.turnId ?? handoverTurnIdRef.current;\n if (stoppedTurnId) stoppedTurnIdRef.current = stoppedTurnId;\n clearHandover();\n // The text that arrived before the stop stays on screen, including what the\n // next frame would have applied; nothing after it does.\n streamDeltasRef.current?.flush();\n streamDeltasRef.current?.close();\n streamDeltasRef.current = null;\n abortControllerRef.current?.abort();\n abortControllerRef.current = null;\n pageActionControllerRef.current?.abort();\n pageActionControllerRef.current = null;\n relayedTurnRef.current = null;\n stopRunning('stopped');\n setAgentStatus(null);\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 pageRefsRef.current = new Map();\n // The answer being written says it was stopped, whatever it got to; an\n // earlier empty one (before a steer) goes.\n const segId = running ? live?.currentAssistantId() : undefined;\n setMessages((prev) =>\n prev.map((m) => (m.id === segId ? { ...m, stopped: true } : m)).filter((m) => !(m.role === 'assistant' && !m.content && !m.stopped)),\n );\n };\n\n /**\n * Stop the turn. A backend whose turns outlive their stream (`cancel` route)\n * is asked to stop it and ends the stream with `cancelled`; without that\n * route, or should it not answer, the panel lets go of the stream itself.\n */\n const handleStopGenerating = () => {\n const withdrawn = withdrawnTurnRef.current;\n if (!isLoadingRef.current && withdrawn) {\n // A turn let go of with the event stream: the backend is told, and its\n // replay is not shown.\n const cancelUrl = getCancelUrl();\n const convId = conversationIdRef.current;\n if (cancelUrl && convId) void postConversation(cancelUrl, convId).catch(() => undefined);\n stoppedTurnIdRef.current = withdrawn.turnId;\n stopRequestedRef.current = true;\n clearResumeWait();\n endWithdrawnTurn('stopped');\n return;\n }\n if (!isLoadingRef.current) {\n stopLocally(false);\n return;\n }\n stopRequestedRef.current = true;\n const cancelUrl = getCancelUrl();\n const convId = conversationIdRef.current;\n // A turn awaited from the event stream has no stream of its own to end with\n // `cancelled`: the backend is told, and the panel lets go at once.\n const awaitingHandover = handoverTurnIdRef.current !== null && !relayedTurnRef.current;\n if (!cancelUrl || !convId || awaitingHandover) {\n if (cancelUrl && convId) void postConversation(cancelUrl, convId).catch(() => undefined);\n stopLocally();\n return;\n }\n if (cancelTimerRef.current !== null) return;\n noticeUntilSendRef.current = false;\n setNotice(null);\n setPendingApprovals(null);\n setApprovalError(null);\n setAgentStatus((prev) => ({ ...prev, status: 'cancelling' }));\n const timer = window.setTimeout(() => stopLocally(), CANCEL_GRACE_MS);\n cancelTimerRef.current = timer;\n // Put back should the backend refuse the Stop: the turn then goes on.\n stopContextRef.current = { statusBefore: agentStatus, approvalsBefore: pendingApprovals, frameSeq: turnFrameSeqRef.current };\n postCancel(timer);\n };\n\n /**\n * Ask the backend to end the turn, for the Stop of *timer*. Sent again once\n * the panel's own turn starts when it went before and was answered `none`\n * (see `cancelBeforeStartRef`): asking twice ends one turn.\n */\n const postCancel = (timer: number) => {\n const cancelUrl = getCancelUrl();\n const convId = conversationIdRef.current;\n if (!cancelUrl || !convId) return;\n // The panel's own send has not started its turn yet: the backend may\n // answer that none runs, and start it right after. `awaiting_send` says\n // so, and the backend may keep the Stop for that send.\n const beforeStart = ownTurnActiveRef.current && !ownTurnStartedRef.current;\n if (beforeStart) cancelBeforeStartRef.current = 'asked';\n // Still this Stop's to settle: the turn has not ended meanwhile.\n const pending = () => cancelTimerRef.current === timer;\n postConversation(cancelUrl, convId, beforeStart ? { awaiting_send: true } : undefined)\n .then(async (res) => {\n if (res.ok) {\n const data: unknown = await res.json().catch(() => null);\n if (!pending()) return;\n const none = !!data && typeof data === 'object' && (data as { turn?: unknown }).turn === 'none';\n if (beforeStart) {\n // Said before the own turn started: `none` says nothing of that\n // turn, which the cancel sent again once it starts reaches; any\n // other answer reached a turn, and nothing goes again.\n if (!none) cancelBeforeStartRef.current = 'off';\n else if (!ownTurnStartedRef.current) cancelBeforeStartRef.current = 'resend';\n else {\n cancelBeforeStartRef.current = 'off';\n postCancel(timer);\n }\n return;\n }\n if (!none) return;\n // No turn was running anywhere: no `cancelled` will come. The turn\n // may have ended as this was on its way, its end not read yet: what\n // it stored is read back.\n stopLocally();\n reloadHistory();\n return;\n }\n // Refused (a turn the backend cannot reach right now): it runs on, and the user is told why.\n const detail = await readRefusalDetail(res);\n if (!pending()) return;\n const before = stopContextRef.current;\n clearCancelRequest();\n stopRequestedRef.current = false;\n // The status as it was, unless a frame of the turn came since: then\n // the turn's own, with its label back if it still says `cancelling`.\n setAgentStatus((prev) => {\n if (prev?.status !== 'cancelling') return prev;\n if (before && turnFrameSeqRef.current === before.frameSeq) return before.statusBefore;\n return { ...prev, status: before?.statusBefore?.status ?? 'thinking' };\n });\n if (before?.approvalsBefore) setPendingApprovals((prev) => prev ?? before.approvalsBefore);\n setNotice(detail ?? t('The response could not be stopped. Try again.'));\n })\n .catch(() => {\n if (pending()) stopLocally();\n });\n };\n\n const skipWaitUrl = getSkipWaitUrl();\n const skipWait = async () => {\n const url = getSkipWaitUrl();\n const convId = conversationIdRef.current;\n if (!url || !convId) return;\n try {\n const res = await postConversation(url, convId);\n if (res.ok) setAgentStatus((prev) => (prev?.status === 'wait_start' ? { status: 'thinking', thinkingContent: prev.thinkingContent } : prev));\n } catch {\n // The wait ends on its own; the button stays for another try.\n }\n };\n\n /**\n * Follow the conversation's event stream (`events` route): turns started\n * elsewhere (another tab, a reload mid-turn, a second device), user\n * messages sent there, new titles and approvals answered there. Reconnects\n * with backoff while the page is visible; a route refused outright stops it.\n * Closed once the page has been hidden for {@link EVENTS_HIDDEN_CLOSE_MS};\n * shown again, the stream reopens (its snapshot replays a turn still\n * running) and the conversation is read back for what ended meanwhile: a\n * relayed turn it let go of and does not replay within\n * {@link EVENTS_RESUME_GRACE_MS} of the page showing, or that a refusal\n * keeps from reopening, ended (`endTurnUnseen`).\n * A refusal of the credentials (401, 403) is tried once more each time the\n * host's `requestHeaders` change, so a refreshed token picks the stream up.\n */\n const eventsUrl = conversationId && historySettledFor === conversationId ? getEventsUrl(conversationId) : null;\n const headersKey = requestHeadersKey(requestHeaders);\n const headersKeyRef = useRef(headersKey);\n headersKeyRef.current = headersKey;\n // The stream refused for its credentials: its URL and the headers it was refused with.\n const [eventsRefused, setEventsRefused] = useState<{ url: string; headersKey: string } | null>(null);\n const [eventsReopen, setEventsReopen] = useState(0);\n useEffect(() => {\n if (!eventsRefused || eventsRefused.url !== eventsUrl || eventsRefused.headersKey === headersKey) return;\n setEventsRefused(null);\n setEventsReopen((n) => n + 1);\n }, [eventsRefused, eventsUrl, headersKey]);\n useEffect(() => {\n if (!eventsUrl) return;\n let stopped = false;\n let connected = false;\n let retry = 0;\n let timer: number | null = null;\n let controller: AbortController | null = null;\n let hiddenTimer: number | null = null;\n // Opened at least once: a stream not followed since then missed frames.\n let opened = false;\n // Frames were missed while the page was hidden: the reopened stream comes\n // with a read-back of the conversation.\n let missed = false;\n\n const connect = async () => {\n timer = null;\n if (stopped || connected) return;\n if (document.visibilityState === 'hidden') {\n if (opened) missed = true;\n return;\n }\n connected = true;\n controller = new AbortController();\n let refused = false;\n let openedAt: number | null = null;\n try {\n const sentHeadersKey = headersKeyRef.current;\n const res = await panelFetch(eventsUrl, {\n headers: { ...(requestHeadersRef.current ?? {}), 'X-Chat-Client-Id': clientId },\n signal: controller.signal,\n });\n if ([401, 403, 404, 405].includes(res.status)) {\n refused = true;\n if ((res.status === 401 || res.status === 403) && !stopped) setEventsRefused({ url: eventsUrl, headersKey: sentHeadersKey });\n // Nothing replays a relayed turn now: how it ended is read back.\n if (!stopped) endTurnUnseenRef.current();\n return;\n }\n if (!res.ok || !res.body) return;\n if (opened) awaitRelayedResumeRef.current();\n opened = true;\n openedAt = Date.now();\n await readSseEvents(res.body, (evt) => {\n // A stream that delivers is healthy: its next drop, or the server's\n // own end of it, is retried from the shortest delay again.\n retry = 0;\n try {\n handleEventFrameRef.current(evt);\n } catch {\n // A frame that could not be applied is skipped, not fatal.\n }\n });\n } catch {\n // Dropped or aborted: reconnected below unless the effect is gone.\n } finally {\n connected = false;\n // So is one that lived a while, keep-alives only: one that opens and\n // closes at once is not, and backs off.\n if (openedAt !== null && Date.now() - openedAt >= EVENTS_HEALTHY_MS) retry = 0;\n if (!stopped && !refused && !missed) {\n const delay = Math.min(EVENTS_RETRY_MAX_MS, EVENTS_RETRY_BASE_MS * 2 ** retry);\n retry += 1;\n timer = window.setTimeout(() => void connect(), delay * (0.5 + Math.random() / 2));\n }\n }\n };\n // Hidden long enough, the stream is let go of, and with it the relayed\n // turn it fed: the reopened stream replays that turn if it still runs.\n const closeWhileHidden = () => {\n hiddenTimer = null;\n if (stopped || document.visibilityState !== 'hidden') return;\n missed = true;\n if (timer !== null) window.clearTimeout(timer);\n timer = null;\n controller?.abort();\n withdrawRelayedTurnRef.current();\n };\n const onVisibilityChange = () => {\n if (document.visibilityState === 'hidden') {\n if (hiddenTimer === null) hiddenTimer = window.setTimeout(closeWhileHidden, EVENTS_HIDDEN_CLOSE_MS);\n return;\n }\n if (hiddenTimer !== null) window.clearTimeout(hiddenTimer);\n hiddenTimer = null;\n if (document.visibilityState !== 'visible') return;\n // A turn let go of while hidden ran on only if the stream replays it\n // soon, whether or not the stream reopens.\n if (withdrawnTurnRef.current) awaitRelayedResumeRef.current();\n if (connected) return;\n if (timer !== null) window.clearTimeout(timer);\n retry = 0;\n if (missed) {\n missed = false;\n // What ended while the stream was closed is only in the history.\n reloadHistory();\n }\n void connect();\n };\n void connect();\n document.addEventListener('visibilitychange', onVisibilityChange);\n return () => {\n stopped = true;\n if (timer !== null) window.clearTimeout(timer);\n if (hiddenTimer !== null) window.clearTimeout(hiddenTimer);\n controller?.abort();\n document.removeEventListener('visibilitychange', onVisibilityChange);\n // A relayed turn nothing feeds any more is over, as far as this panel can tell.\n endRelayedTurnRef.current();\n endWithdrawnTurnRef.current();\n };\n }, [eventsUrl, clientId, eventsReopen, reloadHistory]);\n\n // Held messages given back to the composer, told to the host once the\n // composer shows them (`getDraft` reads it then).\n const returnedRef = useRef<string[]>([]);\n const [returnedTick, setReturnedTick] = useState(0);\n const settleReturned = (text: string) => {\n returnedRef.current.push(text);\n setReturnedTick((n) => n + 1);\n };\n useEffect(() => {\n for (const text of returnedRef.current.splice(0)) onHeldSendSettledRef.current?.(text, 'returned');\n }, [returnedTick]);\n\n // The turn is over, and no longer holds the conversation (see\n // `WIND_DOWN_MS`): what was held goes out as the next one.\n useEffect(() => {\n if (isLoading || turnWithdrawn) return;\n if (stopRequestedRef.current) {\n // A Stop is over with its turn: it says nothing of a later send, which\n // goes out once the turn winds down.\n stopRequestedRef.current = false;\n if (heldMessage) {\n // Stopped: the user takes back what they typed rather than see it go;\n // a host's send is not theirs to take back, and goes nowhere.\n setHeldMessage(null);\n tellNotSent(heldMessage.parts);\n if (!heldMessage.draft && heldMessage.files.length === 0) {\n onHeldSendSettledRef.current?.(heldMessage.text, 'dropped');\n return;\n }\n settleReturned(heldMessage.text);\n returnToComposer(heldMessage, { before: true });\n return;\n }\n }\n if (windingDown || !heldMessage) return;\n setHeldMessage(null);\n // Told once: as the backend takes it, or as it fails or goes nowhere. Held\n // again behind a turn started meanwhile, it is told when that one ends.\n let told = false;\n const tell = (outcome: ChatHeldOutcome) => {\n if (told) return;\n told = true;\n onHeldSendSettledRef.current?.(heldMessage.text, outcome);\n };\n // Each send the host's hook decided while a turn ran is asked again, the\n // turn over, before the message leaves as a new one. One it takes over\n // gives back to the composer what the user typed for it, after what they\n // typed since.\n const kept: HeldPart[] = [];\n const takenOver: HeldPart[] = [];\n for (const part of heldMessage.parts) {\n const { asked } = part;\n if (!asked) {\n kept.push(part);\n continue;\n }\n const decision = applySendDecision(\n asked.text,\n askHost(onBeforeSendRef.current, { text: asked.text, source: asked.source, turnRunning: false, held: true }),\n asked.displayText,\n );\n if (!decision) {\n takenOver.push(part);\n continue;\n }\n kept.push({\n ...part,\n text: decision.text,\n displayText: decision.displayText !== decision.text ? decision.displayText : undefined,\n extras: asked.extras || decision.extras ? { ...asked.extras, ...decision.extras } : undefined,\n asked: undefined,\n });\n }\n tellNotSent(takenOver);\n const givenBack = takenOver.filter((part) => part.draft || part.files.length > 0);\n for (const part of givenBack) returnToComposer(part, { before: false });\n if (kept.length === 0) {\n if (givenBack.length > 0) settleReturned(heldMessage.text);\n else tell('dropped');\n return;\n }\n const { text, draft, files, refs, mentions, extras, displayText } = heldMessageOf(kept);\n void handleSendMessageRef\n .current({\n text,\n draft,\n files,\n refs,\n mentions,\n extras,\n displayText,\n decided: true,\n onSent: () => {\n tell('sent');\n for (const part of kept) askHost(part.onSent);\n },\n onNotSent: () => tellNotSent(kept),\n })\n .then(\n (outcome) => {\n if (outcome === 'held') return;\n const sent = outcome === 'sent' || outcome === 'steered';\n tell(sent ? 'sent' : outcome === 'failed' ? 'failed' : 'dropped');\n if (!sent) tellNotSent(kept);\n },\n () => {\n tell('failed');\n tellNotSent(kept);\n },\n );\n }, [isLoading, turnWithdrawn, windingDown, heldMessage]);\n\n const handleSendMessageRef = useRef(handleSendMessage);\n handleSendMessageRef.current = handleSendMessage;\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(() => {\n persistDraft(storageKeyPrefix, conversationId, inputValue, conversationRefs);\n }, DRAFT_PERSIST_DELAY_MS);\n return () => window.clearTimeout(id);\n }, [inputValue, conversationId, conversationRefs, storageKeyPrefix]);\n\n // A panel closed mid-stream has no state left for the stream's frames to\n // update. Its page actions stop, so a hover fallback cannot click the host\n // page after the panel closed; the stream itself runs on, which is how its\n // answer is in history when the panel reopens.\n useEffect(\n () => () => {\n streamDeltasRef.current?.close();\n pageActionControllerRef.current?.abort();\n // The own stream read on for a stored end has no panel left to give it to.\n storedEndControllerRef.current?.abort();\n if (windDownRef.current) window.clearTimeout(windDownRef.current.timer);\n\n if (handoverTimerRef.current !== null) window.clearTimeout(handoverTimerRef.current);\n if (resumeWaitRef.current) window.clearTimeout(resumeWaitRef.current.timer);\n // A message still held goes nowhere with the panel.\n cancelHeld();\n },\n [],\n );\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, conversationRefs });\n draftFlushRef.current = { conversationId, inputValue, conversationRefs };\n useEffect(\n () => () => {\n const { conversationId: id, inputValue: value, conversationRefs: refs } = draftFlushRef.current;\n persistDraft(storageKeyPrefix, id, value, refs);\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 panelFetch(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 turnRunning: isLoading || turnWithdrawn,\n sendsHeld: !isLoading && (windingDown || turnWithdrawn),\n turnEnd,\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 skipWait: skipWaitUrl ? skipWait : null,\n readPendingApprovals: pendingApprovalsBase ? readPendingApprovals : null,\n heldMessage,\n notice,\n cancelHeldMessage: cancelHeld,\n reloadHistory,\n mergeTurnHistory,\n settleUnseenEnd,\n agentMentions,\n addAgentMention,\n handleNewChat,\n pendingWorkspaceId,\n clearPendingWorkspace,\n handleStopGenerating,\n conversationRefs,\n addConversationRef,\n setAttachedFiles,\n setMessages,\n setContextUsage,\n updateConversationId,\n handleSwitchConversation,\n };\n}\n","/**\n * Streamed answer text and reasoning, applied to React state at most once per\n * animation frame.\n *\n * A backend can emit one event per model delta, dozens a second: applied one by\n * one, each re-renders the panel and re-parses the live answer, far more often\n * than the screen can show. The deltas are gathered here and applied together\n * on the next frame. Every other event is handled after a `flush()`, so state\n * still changes in the order the events arrived.\n *\n * A hidden tab runs no frames, and needs none: what waits is the text itself,\n * not a queue of events, and the first frame once the tab is visible applies\n * it - or, before that, the next event of another kind or the end of the\n * stream.\n */\nexport interface StreamDeltas {\n /** The answer text grew; the applier reads the text itself. */\n text: () => void;\n /** A chunk of reasoning prose arrived. */\n thinking: (chunk: string) => void;\n /** Apply what is pending now. */\n flush: () => void;\n /** Drop what is pending and ignore any later delta: the stream is over or abandoned. */\n close: () => void;\n}\n\nexport function createStreamDeltas(apply: (textChanged: boolean, thinking: string) => void): StreamDeltas {\n let textChanged = false;\n let thinking = '';\n let frame: number | null = null;\n let closed = false;\n\n const cancelFrame = () => {\n if (frame !== null) cancelAnimationFrame(frame);\n frame = null;\n };\n const flush = () => {\n cancelFrame();\n if (!textChanged && !thinking) return;\n const changed = textChanged;\n const chunk = thinking;\n textChanged = false;\n thinking = '';\n apply(changed, chunk);\n };\n const schedule = () => {\n if (frame !== null) return;\n if (typeof requestAnimationFrame !== 'function') {\n flush();\n return;\n }\n frame = requestAnimationFrame(() => {\n frame = null;\n flush();\n });\n };\n\n return {\n text: () => {\n if (closed) return;\n textChanged = true;\n schedule();\n },\n thinking: (chunk) => {\n if (closed) return;\n thinking += chunk;\n schedule();\n },\n flush,\n close: () => {\n closed = true;\n cancelFrame();\n textChanged = false;\n thinking = '';\n },\n };\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { ApiEndpoints, BackendType, XtmAgent } from '../types';\nimport { readStorage, STORAGE_KEYS, storageKey, writeStorage } from '../utils/storage';\nimport { usePanelFetch } from './usePanelRequests';\n\ninterface UseAgentsOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n requestHeaders?: Record<string, string>;\n /** Namespace of the browser-storage keys (see `storageKeyPrefix` on the panel). */\n storageKeyPrefix?: string;\n /** Agent selected once the list loads, by id or slug; wins over the remembered one. */\n initialAgent?: string;\n /** Called when the user (or the host, through the panel handle) picks another agent. */\n onAgentChange?: (agent: XtmAgent) => void;\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 /** True once the list is known (loaded, failed, or not fetched at all). */\n agentsSettled: boolean;\n /** True when the host serves no agent list: conversations go without one. */\n agentsDisabled: 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 /** Read the agent list again: agents created or changed since. */\n reloadAgents: () => 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\n/** The agent named by *key*, matched on id first, then slug. */\nexport function findAgent(agents: XtmAgent[], key: string | null | undefined): XtmAgent | undefined {\n if (!key) return undefined;\n return agents.find((a) => a.id === key) ?? agents.find((a) => a.slug === key);\n}\n\nexport function useAgents({\n apiBaseUrl,\n apiEndpoints,\n backendType = 'rest',\n requestHeaders,\n storageKeyPrefix,\n initialAgent,\n onAgentChange,\n}: UseAgentsOptions): UseAgentsReturn {\n const panelFetch = usePanelFetch();\n const agentsDisabled = apiEndpoints?.agents === null || !!apiEndpoints?.singleEndpoint || backendType === 'legacy';\n const agentStorageKey = storageKey(storageKeyPrefix, STORAGE_KEYS.agentSlug);\n const [agents, setAgents] = useState<XtmAgent[]>([]);\n const [agentsLoading, setAgentsLoading] = useState(false);\n const [agentsError, setAgentsError] = useState(false);\n const [agentsSettled, setAgentsSettled] = useState(agentsDisabled);\n // Bumped to read the list again (`reloadAgents` on the panel handle).\n const [reloadNonce, setReloadNonce] = useState(0);\n const reloadAgents = useCallback(() => setReloadNonce((n) => n + 1), []);\n const [selectedAgent, setSelectedAgent] = useState<XtmAgent | null>(null);\n const [agentMenuOpen, setAgentMenuOpen] = useState(false);\n // Only the latest read lands: a slow earlier one cannot overwrite it.\n const requestSeqRef = useRef(0);\n\n useEffect(() => {\n // Skip agents fetch if disabled, using single endpoint mode, or legacy backend\n if (agentsDisabled) {\n setAgentsSettled(true);\n return;\n }\n const agentsUrl = `${apiBaseUrl}${apiEndpoints?.agents ?? '/chat/agents'}`;\n const seq = ++requestSeqRef.current;\n const isLatest = () => seq === requestSeqRef.current;\n setAgentsLoading(true);\n panelFetch(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 if (!isLatest()) return;\n const list = parseAgents(data);\n setAgents(list);\n setAgentsError(false);\n // No agent left: none is picked, so nothing is sent in a removed one's name.\n if (list.length === 0) {\n setSelectedAgent(null);\n return;\n }\n const pickStarting = () => {\n const savedSlug = readStorage('local', agentStorageKey);\n return findAgent(list, initialAgent) ?? (savedSlug ? list.find((a) => a.slug === savedSlug) : undefined) ?? list[0];\n };\n // A reload keeps the agent picked, as the list now describes it,\n // unless it is gone from the list.\n setSelectedAgent((prev) => (prev ? (list.find((a) => a.id === prev.id) ?? pickStarting()) : pickStarting()));\n })\n .catch(() => {\n if (!isLatest()) return;\n setAgentsError(true);\n // A failed reload keeps the list it had.\n if (reloadNonce === 0) setAgents([]);\n })\n .finally(() => {\n if (!isLatest()) return;\n setAgentsLoading(false);\n setAgentsSettled(true);\n });\n }, [apiBaseUrl, apiEndpoints, backendType, requestHeaders, agentsDisabled, reloadNonce]);\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) writeStorage('local', agentStorageKey, agent.slug);\n setAgentMenuOpen(false);\n onAgentChange?.(agent);\n onSwitch?.();\n };\n\n return {\n agents,\n agentsLoading,\n agentsError,\n agentsSettled,\n agentsDisabled,\n selectedAgent,\n setSelectedAgent,\n agentMenuOpen,\n setAgentMenuOpen,\n handleSwitchAgent,\n reloadAgents,\n };\n}\n","import { useCallback, useState } from 'react';\nimport type { ApiEndpoints, BackendType, ChatConversationSummary } from '../types';\nimport { readRefusalDetail } from '../utils/refusal';\nimport { revertMove } from '../utils/workspaces';\nimport { usePanelFetch } from './usePanelRequests';\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 * File a conversation into a workspace (`null` takes it out of one) with\n * `PATCH {history}/{id}`. Resolves to `null` on success, or the backend's\n * reason (an empty string when it gave none).\n */\n moveConversation: (id: string, workspaceId: string | null) => Promise<string | null>;\n /** Delete every conversation (`apiEndpoints.clearAll`); `null` when the host offers no route. */\n clearConversations: (() => Promise<boolean>) | null;\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 // Present (string or null) only when the backend reports filing at all.\n const workspaceId = typeof c.workspace_id === 'string' && c.workspace_id ? c.workspace_id : c.workspace_id === null ? null : undefined;\n return { conversationId: id, title, updatedAt, messageCount, agentName, workspaceId };\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 panelFetch = usePanelFetch();\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 panelFetch(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 panelFetch(`${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 clearAllUrl = historyEnabled && apiEndpoints?.clearAll ? `${apiBaseUrl}${apiEndpoints.clearAll}` : null;\n const clearConversations = useCallback(async (): Promise<boolean> => {\n if (!clearAllUrl) return false;\n try {\n const res = await panelFetch(clearAllUrl, { method: 'DELETE', headers: { ...(requestHeaders ?? {}) } });\n if (!res.ok) return false;\n setConversations([]);\n return true;\n } catch {\n return false;\n }\n }, [clearAllUrl, requestHeaders]);\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 panelFetch(`${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 const moveConversation = useCallback(\n async (id: string, workspaceId: string | null): Promise<string | null> => {\n if (!historyEnabled) return '';\n // Optimistic, like a rename: the row moves under the pointer that\n // dropped it, and goes back if the backend refuses. Only this row, and\n // only while it is still where this move put it: other moves may have\n // landed, or the list been refreshed, while this one was in flight.\n const previous = conversations.find((c) => c.conversationId === id)?.workspaceId;\n const revert = () => setConversations((prev) => revertMove(prev, id, workspaceId, previous));\n setConversations((prev) => prev.map((c) => (c.conversationId === id ? { ...c, workspaceId } : c)));\n try {\n const res = await panelFetch(`${sessionsUrl}/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({ workspace_id: workspaceId }),\n });\n if (!res.ok) {\n revert();\n return (await readRefusalDetail(res)) ?? '';\n }\n return null;\n } catch {\n revert();\n return '';\n }\n },\n [historyEnabled, sessionsUrl, requestHeaders, conversations],\n );\n\n return {\n historyEnabled,\n conversations,\n conversationsLoading,\n refreshConversations,\n deleteConversation,\n renameConversation,\n moveConversation,\n clearConversations: clearAllUrl ? clearConversations : null,\n };\n}\n","import { useEffect, useRef, useState } from 'react';\nimport type { ChatPanelMode } from '../types';\nimport { readStorage, STORAGE_KEYS, storageKey, writeStorage } from '../utils/storage';\n\nconst SIDEBAR_WIDTH = 400;\nconst MAX_SIDEBAR_RATIO = 0.4;\n\ninterface UseSidebarResizeOptions {\n mode: ChatPanelMode;\n resizable: boolean;\n onWidthChange?: (width: number) => void;\n onResizeStart?: () => void;\n onResizeEnd?: () => void;\n storageKeyPrefix?: string;\n}\n\ninterface UseSidebarResizeReturn {\n sidebarWidth: number;\n handleResizeStart: (e: React.MouseEvent) => void;\n defaultWidth: number;\n isResizing: boolean;\n}\n\nexport function useSidebarResize({\n mode,\n resizable,\n onWidthChange,\n onResizeStart,\n onResizeEnd,\n storageKeyPrefix,\n}: UseSidebarResizeOptions): UseSidebarResizeReturn {\n const widthStorageKey = storageKey(storageKeyPrefix, STORAGE_KEYS.sidebarWidth);\n const [sidebarWidth, setSidebarWidth] = useState<number>(() => {\n const stored = readStorage('local', widthStorageKey);\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 writeStorage('local', widthStorageKey, 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, widthStorageKey]);\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';\nimport { translate } from '../utils';\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 ? translate(t, '{agent} has finished', { agent: agentName }) : 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';\nimport { parseQuota } from '../utils/quota';\nimport { usePanelFetch } from './usePanelRequests';\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\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 panelFetch = usePanelFetch();\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 panelFetch(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 panelFetch(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 type { ChatQuotaStatus, Translate } from '../types';\n\n/**\n * The quota the composer indicator shows, as the quota route sends it.\n *\n * Kept free of local runtime imports so `node --test` runs it as is.\n */\n\n/** Defensive parse: an unexpected payload shows no indicator rather than a wrong one. */\nexport function 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 ...(q.scope === 'global' || q.scope === 'user' ? { scope: q.scope } : {}),\n };\n}\n\n/**\n * The period a limit applies to, as the tooltip words it: the periods XTM One\n * sets a quota for are translated, anything else is shown as sent.\n */\nexport function quotaPeriodLabel(period: string, t: Translate): string {\n switch (period) {\n case 'daily':\n return t('today');\n case 'monthly':\n return t('this month');\n case 'yearly':\n return t('this year');\n default:\n return period;\n }\n}\n","import { useEffect, useState } from 'react';\nimport type { ApiEndpoints, BackendType } from '../types';\nimport { usePanelFetch } from './usePanelRequests';\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 panelFetch = usePanelFetch();\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 panelFetch(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 { useCallback, useInsertionEffect, useRef } from 'react';\n\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const keys = Object.keys(a);\n if (keys.length !== Object.keys(b).length) return false;\n return keys.every(\n (key) => Object.prototype.hasOwnProperty.call(b, key) && Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\n/**\n * `value`, keeping the previous object while its keys and values are the same.\n *\n * For plain-data props a host builds inline (headers, endpoint paths, page\n * context): a new but equal object on every host render would otherwise break\n * the memoized message rows and re-run every effect keyed on it — refetching\n * agents, prompts, quota and suggestions, and every image fetched with the\n * headers.\n */\nexport function useShallowStable<T>(value: T): T {\n const ref = useRef(value);\n if (!shallowEqual(ref.current, value)) ref.current = value;\n return ref.current;\n}\n\n/**\n * A function with a stable identity that always calls the latest `callback`,\n * or `undefined` while there is none (callers test for presence to decide what\n * to render).\n */\nexport function useLatestCallback<Args extends unknown[], Result>(\n callback: ((...args: Args) => Result) | undefined,\n): ((...args: Args) => Result) | undefined {\n const ref = useRef(callback);\n // Updated before any layout or passive effect runs, so an effect of this\n // render never calls the previous callback.\n useInsertionEffect(() => {\n ref.current = callback;\n });\n const stable = useCallback((...args: Args) => ref.current?.(...args) as Result, []);\n return callback ? stable : undefined;\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}\nexport interface SpeechRecognitionEventLike {\n resultIndex: number;\n results: { length: number; [index: number]: SpeechRecognitionResultLike };\n}\n/** What a recognition error says: its code (`no-speech`, `not-allowed`, ...). */\nexport interface SpeechRecognitionErrorEventLike {\n error?: string;\n}\nexport interface SpeechRecognitionLike {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n start: () => void;\n stop: () => void;\n abort: () => void;\n onresult: ((event: SpeechRecognitionEventLike) => void) | null;\n onerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;\n onend: (() => void) | null;\n}\n\nexport interface DictationEvents {\n /** A finalised phrase, trimmed, to append to the draft. */\n onFinal: (text: string) => void;\n /** Words heard but not final yet; an empty string clears the preview. */\n onInterim: (text: string) => void;\n onListeningChange: (listening: boolean) => void;\n}\n\nexport interface DictationController {\n /** Start listening, or stop and commit the phrase already heard. */\n toggle: () => void;\n /** Stop listening and drop whatever the session still delivers. */\n cancel: () => void;\n /** Release the recogniser; no event is reported afterwards. */\n dispose: () => void;\n}\n\n/**\n * - `listening`: a session the user wants; the engine ending it after a pause\n * starts it again.\n * - `stopping`: the user stopped it; the phrase in flight is still committed.\n * - `discarding`: cancelled; everything it delivers until its `end` is dropped.\n */\ntype Phase = 'idle' | 'listening' | 'stopping' | 'discarding';\n\n/**\n * Drives one recogniser through successive dictation sessions.\n *\n * A session keeps emitting after `stop()` / `abort()` until its `end` event,\n * and no event says which session it belongs to. The recogniser also refuses\n * `start()` until that `end`, so every event is read against the phase of the\n * one session it can belong to, and a start asked for while the previous\n * session is still ending waits for that `end` instead of failing.\n */\nexport function createDictationController(recognition: SpeechRecognitionLike, events: DictationEvents): DictationController {\n let phase: Phase = 'idle';\n let startQueued = false;\n let disposed = false;\n let reported = false;\n\n const isOn = () => phase === 'listening' || startQueued;\n\n const report = () => {\n if (disposed || isOn() === reported) return;\n reported = isOn();\n events.onListeningChange(reported);\n };\n\n const tryStart = () => {\n try {\n recognition.start();\n return true;\n } catch {\n return false;\n }\n };\n\n const start = () => {\n if (tryStart()) phase = 'listening';\n else if (phase !== 'idle') startQueued = true;\n report();\n };\n\n const stop = () => {\n startQueued = false;\n if (phase === 'listening') {\n phase = 'stopping';\n events.onInterim('');\n try {\n recognition.stop();\n } catch {\n phase = 'idle';\n }\n }\n report();\n };\n\n const cancel = () => {\n if (disposed) return;\n startQueued = false;\n if (phase === 'listening' || phase === 'stopping') {\n phase = 'discarding';\n events.onInterim('');\n try {\n recognition.abort();\n } catch {\n phase = 'idle';\n }\n }\n report();\n };\n\n recognition.onresult = (event) => {\n if (phase !== 'listening' && phase !== 'stopping') return;\n let finalText = '';\n let interimText = '';\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 const phrase = finalText.trim();\n if (phrase) events.onFinal(phrase);\n if (phase === 'listening') events.onInterim(interimText.trim());\n };\n\n recognition.onerror = () => {\n // Permission denied, no microphone, network failure: let the session end\n // rather than restart it into the same failure.\n if (phase === 'listening') {\n phase = 'stopping';\n events.onInterim('');\n }\n report();\n };\n\n recognition.onend = () => {\n events.onInterim('');\n if (phase === 'listening') {\n if (tryStart()) return;\n phase = 'idle';\n } else if (startQueued) {\n startQueued = false;\n phase = tryStart() ? 'listening' : 'idle';\n } else {\n phase = 'idle';\n }\n report();\n };\n\n return {\n toggle: () => {\n if (disposed) return;\n if (isOn()) stop();\n else start();\n },\n cancel,\n dispose: () => {\n if (disposed) return;\n disposed = true;\n recognition.onresult = null;\n recognition.onerror = null;\n recognition.onend = null;\n if (phase !== 'idle') {\n try {\n recognition.abort();\n } catch {\n /* already ended */\n }\n }\n phase = 'idle';\n startQueued = false;\n },\n };\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { createDictationController, type DictationController, type SpeechRecognitionLike } from './dictationController';\n\ntype SpeechRecognitionCtor = new () => SpeechRecognitionLike;\n\n/** The browser's speech recogniser, vendor-prefixed or not; `null` without one. */\nexport function 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 /** Start listening, or stop and still commit the phrase already heard. */\n toggle: () => void;\n /** Stop listening and drop anything still on its way, e.g. once the draft is sent. */\n cancel: () => 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 controllerRef = useRef<DictationController | null>(null);\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 const controller = createDictationController(recognition, {\n onFinal: (text) => onFinalTextRef.current(text),\n onInterim: setInterim,\n onListeningChange: setListening,\n });\n controllerRef.current = controller;\n return () => {\n controllerRef.current = null;\n controller.dispose();\n setListening(false);\n setInterim('');\n };\n }, []);\n\n const toggle = useCallback(() => controllerRef.current?.toggle(), []);\n const cancel = useCallback(() => controllerRef.current?.cancel(), []);\n\n return { supported, listening, interim, toggle, cancel };\n}\n","/**\n * Which chat frames answer a voice call's spoken question.\n *\n * The panel reads two streams: the message stream of the turn it started\n * (frames not `relayed`) and the conversation's event stream, which relays the\n * turns it did not start (`relayed: true`). Only the turn that answers the\n * question may resolve it, and only with its FINAL `done`: the panel's own\n * turn, or - when the question was folded into a turn already running - the\n * turn `folded_into_active_turn` (or a steer's answer) names by `turn_id`. A\n * mid-turn segment commit (an intermediate `done`) never cuts the answer\n * short, and every other relayed frame is ignored.\n *\n * Only the agent the question addresses - the conversation's - is spoken. A\n * turn several mentioned agents answer carries `answered_by` on each reply:\n * the replies of the others are handed over as text with their name, never\n * spoken as the addressed agent's answer and never dropped. On such a turn the\n * addressed agent's own reply is attributed to it too, so the attribution is\n * compared with the addressed agent rather than read as \"someone else\".\n */\n\nimport { parseAnsweredBy } from './protocols/parseRestEvent';\n\n/** A chat frame, as far as a voice call reads it. */\nexport interface VoiceFrame {\n type?: unknown;\n relayed?: unknown;\n turn_id?: unknown;\n intermediate?: unknown;\n content?: unknown;\n status?: unknown;\n agent_id?: unknown;\n answered_by?: unknown;\n mention_failed?: unknown;\n [field: string]: unknown;\n}\n\n/** A reply another agent wrote in the answering turn: shown with its name, never spoken. */\nexport interface VoiceAgentReply {\n agentId: string;\n agentName: string;\n /** A note written in place of an agent that did not answer (`mention_failed`). */\n failed: boolean;\n text: string;\n}\n\n/** The answer a voice call is waiting for. */\nexport interface PendingVoiceAnswer {\n /** The turn that answers the question, once known. */\n turnId: string | null;\n /** The question joined a turn already running: that turn's earlier segments answer what it was already doing. */\n folded: boolean;\n /** The agent the question addresses (the conversation's; `null` when none is known). A transfer in the turn moves it. */\n agentId: string | null;\n /** The addressed agent's committed replies so far: every one of its own turn (spoken with the final one), the last one of a folded turn. */\n segments: string[];\n /** The other agents' replies in the turn, in order, each with its text. */\n others: VoiceAgentReply[];\n}\n\n/** One line an answered question adds to the call's transcript. */\nexport interface VoiceTranscriptLine {\n content: string;\n /** Another agent's reply: shown with its name, never spoken. */\n agent?: { name: string; failed: boolean };\n}\n\n/**\n * The lines an answer adds to the call's transcript: the addressed agent's\n * answer, then every other agent's reply of the turn in order - never an\n * empty line.\n */\nexport function answerTranscriptLines(answer: { text: string; others: VoiceAgentReply[] }): VoiceTranscriptLine[] {\n const lines: VoiceTranscriptLine[] = answer.text.trim() ? [{ content: answer.text }] : [];\n for (const reply of answer.others) {\n if (!reply.text.trim()) continue;\n lines.push({ content: reply.text, agent: { name: reply.agentName, failed: reply.failed } });\n }\n return lines;\n}\n\nexport type VoiceVerdict =\n /** Not about the pending answer (another turn, a keep-alive). */\n | { kind: 'ignore' }\n /** The answering turn is still working: keep waiting. */\n | { kind: 'progress' }\n /** `text` is the addressed agent's answer (empty when it said nothing after the question); `others` the rest of the turn. */\n | { kind: 'answer'; text: string; others: VoiceAgentReply[] }\n /** The answering turn ended without an answer, or none will come. */\n | { kind: 'failed' };\n\nconst TURN_PROGRESS = new Set(['thinking', 'status', 'stream', 'turn_started']);\n\n/** What the idle check learns of the turn a spoken question waits on. */\nexport type TurnProbe = 'running' | 'over' | 'unknown';\n\n/**\n * Read whether the turn still runs, for the idle check. A read that failed\n * says nothing about the turn (`unknown`): the wait goes on and the check is\n * asked again, rather than failing the answer of a long tool call over a\n * transient error.\n */\nexport async function probeTurn(read: () => Promise<{ running: boolean } | null>): Promise<TurnProbe> {\n try {\n const state = await read();\n if (!state) return 'unknown';\n return state.running ? 'running' : 'over';\n } catch {\n return 'unknown';\n }\n}\n\nexport function newPendingAnswer(agentId: string | null = null): PendingVoiceAnswer {\n return { turnId: null, folded: false, agentId, segments: [], others: [] };\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\n/** The agent a reply of a multi-agent turn names (`answered_by`), when it gives both its id and its name. */\nfunction attribution(frame: VoiceFrame): { agentId: string; agentName: string } | null {\n const by = parseAnsweredBy(frame.answered_by);\n return by?.agentId && by.agentName?.trim() ? { agentId: by.agentId, agentName: by.agentName } : null;\n}\n\n/**\n * Read one frame against the pending answer. Mutates `pending` as the answer\n * builds up (the answering turn once named, each committed segment, the other\n * agents' replies).\n */\nexport function classifyVoiceFrame(pending: PendingVoiceAnswer, frame: VoiceFrame): VoiceVerdict {\n const turnId = typeof frame.turn_id === 'string' && frame.turn_id !== '' ? frame.turn_id : null;\n if (frame.relayed === true) {\n // A turn the panel did not start answers the question only when the\n // question was folded into it.\n if (pending.turnId === null || turnId !== pending.turnId) return { kind: 'ignore' };\n } else if (frame.type === 'folded_into_active_turn') {\n // The question joined a turn that was already running: its frames follow\n // under this id. Queued for the next turn with no turn running\n // (`folded: false`), nothing answers it now. Without an id, the idle\n // check settles the wait.\n if (turnId !== null) {\n pending.turnId = turnId;\n pending.folded = true;\n } else if (frame.folded === false) {\n return { kind: 'failed' };\n }\n return { kind: 'progress' };\n } else if (turnId !== null && pending.turnId === null) {\n // The panel's own turn names itself once it runs.\n pending.turnId = turnId;\n }\n const type = frame.type;\n if (\n type === 'status' &&\n frame.status === 'transfer_start' &&\n typeof frame.agent_id === 'string' &&\n frame.agent_id &&\n // Only a transfer in the answering turn - named above - moves the\n // addressed agent; a frame naming no turn counts too.\n (turnId === null || turnId === pending.turnId)\n ) {\n // The conversation now belongs to the agent it was transferred to.\n pending.agentId = frame.agent_id;\n }\n if (typeof type === 'string' && TURN_PROGRESS.has(type)) return { kind: 'progress' };\n if (type === 'done') {\n const content = text(frame.content);\n const final = frame.intermediate !== true;\n const by = attribution(frame);\n if (by !== null && by.agentId !== pending.agentId) {\n // An agent the question did not address answered in this turn.\n if (content) pending.others.push({ ...by, failed: frame.mention_failed === true, text: content });\n if (!final) return { kind: 'progress' };\n return { kind: 'answer', text: pending.segments.join('\\n\\n'), others: pending.others };\n }\n if (!final) {\n // A folded turn's segments answer what it was doing before the\n // question joined it; the last of them stands for its reply only when\n // another agent's reply ends the turn.\n if (content) pending.segments = pending.folded ? [content] : [...pending.segments, content];\n return { kind: 'progress' };\n }\n const spoken = pending.folded ? [content] : [...pending.segments, content];\n return { kind: 'answer', text: spoken.filter(Boolean).join('\\n\\n'), others: pending.others };\n }\n if (type === 'cancelled' || type === 'error') return { kind: 'failed' };\n return { kind: 'ignore' };\n}\n\n/** Words of *value*, lower-cased, for comparing what was heard with what is being said. */\nfunction words(value: string): string[] {\n return value.toLowerCase().match(/[\\p{L}\\p{N}]+/gu) ?? [];\n}\n\n/**\n * Whether speech heard while the call speaks is the user talking over it\n * (barge-in) rather than its own voice coming back through the microphone:\n * at least two words, and not mostly words of what is being said.\n */\nexport function isBargeIn(heard: string, speaking: string): boolean {\n const said = words(heard);\n if (said.length < 2) return false;\n const spoken = new Set(words(speaking));\n const echoed = said.filter((w) => spoken.has(w)).length;\n return echoed / said.length < 0.6;\n}\n","import type { ChatSendOutcome } from '../types';\nimport type { SpeechRecognitionLike } from './dictationController';\nimport {\n answerTranscriptLines,\n classifyVoiceFrame,\n isBargeIn,\n newPendingAnswer,\n type PendingVoiceAnswer,\n type TurnProbe,\n type VoiceAgentReply,\n type VoiceFrame,\n type VoiceTranscriptLine,\n} from './voiceTurn';\n\n/**\n * - `listening`: the microphone is open; one second of silence after a phrase sends it.\n * - `processing`: the question is out, its answer awaited.\n * - `speaking`: the answer is read out; speech over it (barge-in) takes the turn back.\n * - `error`: the question got no answer; listening resumes shortly.\n * - `ended`: the call is over.\n */\nexport type VoiceCallPhase = 'listening' | 'processing' | 'speaking' | 'error' | 'ended';\n\n/** One line of the call's transcript: what the user said, or an answer. */\ninterface VoiceCallLine extends VoiceTranscriptLine {\n role: 'user' | 'assistant';\n}\n\nexport interface VoiceCallSnapshot {\n phase: VoiceCallPhase;\n muted: boolean;\n /** What is being said, not sent yet. */\n heard: string;\n lines: VoiceCallLine[];\n}\n\nexport interface VoiceCallDeps {\n /** The browser's recogniser. */\n recognition: SpeechRecognitionLike;\n lang?: string;\n /**\n * Send a question as the user. `onAccepted` once the backend takes it as a\n * new turn (or the held message goes out); `onNotSent` when, held, it never\n * goes out; `onSteered` with the turn it joined (`null`: none named), and\n * whether the backend said no turn took it (it waits for the next one).\n */\n send: (\n text: string,\n hooks: { onAccepted: () => void; onNotSent: () => void; onSteered: (turnId: string | null, unanswered: boolean) => void },\n ) => Promise<ChatSendOutcome>;\n /** Say *text* aloud; resolves once said, or as soon as *signal* aborts. */\n speak: (text: string, signal: AbortSignal) => Promise<void>;\n /** The words to say of an answer's markdown. */\n speakable: (text: string) => string;\n /** The text of an answer as the panel shows it (the host's transform). */\n present: (text: string) => string;\n /** Whether the turn the question waits on still runs. */\n probe: () => Promise<TurnProbe>;\n /** Stop the running turn: the user cancelled the question. */\n stopTurn: () => void;\n /** The agent a question addresses, read as it is sent. */\n addressedAgentId: () => string | null;\n onChange: (snapshot: VoiceCallSnapshot) => void;\n setTimer?: (fn: () => void, ms: number) => unknown;\n clearTimer?: (id: unknown) => void;\n}\n\n/** Silence after a phrase that sends it. */\nexport const VOICE_SILENCE_MS = 1000;\n/** How long the answering turn may send nothing before the call asks whether it still runs. */\nexport const VOICE_ANSWER_IDLE_MS = 120_000;\n/** How long a question that got no answer says so before listening resumes. */\nexport const VOICE_RETRY_MS = 2000;\n/** Pause before reopening a recogniser that failed, and how many failures in a row, with nothing heard between, mute it. */\nconst RECOGNITION_RETRY_MS = 300;\nconst RECOGNITION_MAX_FAILURES = 5;\n/** The recogniser's errors of a working session: silence, or its session aborted. They never mute it. */\nconst RECOGNITION_ROUTINE_ERRORS: ReadonlySet<string> = new Set(['no-speech', 'aborted']);\n\nexport interface VoiceCallController {\n start: () => void;\n end: () => void;\n /** A frame of the message stream (`relayed: false`) or the event stream (`relayed: true`). */\n frame: (frame: Record<string, unknown>, relayed: boolean) => void;\n /** Tap on the call: stops the answer being said, or cancels the question awaited (stopping its turn). */\n interrupt: () => void;\n /** The turn was stopped from outside the call: drop the question, stop speaking, listen. */\n cancel: () => void;\n toggleMute: () => void;\n}\n\n/**\n * One voice call: listen, send after a second of silence, wait for the answer\n * of the turn that answers the question (`classifyVoiceFrame`), say it, listen\n * again. The panel sends and streams; the call only reads the frames it is\n * handed.\n */\nexport function createVoiceCall(deps: VoiceCallDeps): VoiceCallController {\n const setTimer = deps.setTimer ?? ((fn: () => void, ms: number) => window.setTimeout(fn, ms));\n const clearTimer = deps.clearTimer ?? ((id: unknown) => window.clearTimeout(id as number));\n const recognition = deps.recognition;\n\n let phase: VoiceCallPhase = 'listening';\n let muted = false;\n let heard = '';\n let lines: VoiceCallLine[] = [];\n // Final phrases of the question being said.\n let said = '';\n // The question awaited: its answer, whether its frames count yet, its number.\n let pending: PendingVoiceAnswer | null = null;\n let armed = false;\n let question = 0;\n let speech: AbortController | null = null;\n let speaking = '';\n let silenceTimer: unknown = null;\n let idleTimer: unknown = null;\n let retryTimer: unknown = null;\n let restartTimer: unknown = null;\n let recognitionRunning = false;\n // The recogniser's session ended on an error: it is reopened after a pause.\n let recognitionFailed = false;\n let recognitionFailures = 0;\n\n const emit = () => deps.onChange({ phase, muted, heard, lines });\n const clear = (id: unknown) => {\n if (id !== null) clearTimer(id);\n return null;\n };\n\n const wantsRecognition = () => !muted && (phase === 'listening' || phase === 'speaking');\n\n const syncRecognition = () => {\n if (wantsRecognition()) {\n if (recognitionRunning || restartTimer !== null) return;\n try {\n recognition.start();\n recognitionRunning = true;\n } catch {\n // Still ending its previous session: `onend` starts it again.\n }\n } else if (recognitionRunning) {\n try {\n recognition.stop();\n } catch {\n // Already ending.\n }\n }\n };\n\n const setPhase = (next: VoiceCallPhase) => {\n phase = next;\n syncRecognition();\n emit();\n };\n\n const stopSpeaking = () => {\n speech?.abort();\n speech = null;\n speaking = '';\n };\n\n const listen = () => {\n silenceTimer = clear(silenceTimer);\n retryTimer = clear(retryTimer);\n said = '';\n heard = '';\n setPhase('listening');\n };\n\n const settle = () => {\n idleTimer = clear(idleTimer);\n pending = null;\n armed = false;\n };\n\n const fail = () => {\n settle();\n setPhase('error');\n retryTimer = setTimer(() => {\n retryTimer = null;\n if (phase === 'error') listen();\n }, VOICE_RETRY_MS);\n };\n\n const armIdleCheck = () => {\n idleTimer = clear(idleTimer);\n const awaited = pending;\n idleTimer = setTimer(() => {\n idleTimer = null;\n // Silent for a long while: a turn still running (a long tool call) keeps\n // the wait, and so does a check that could not read it; one that is over\n // without an answer frame ends it.\n void deps.probe().then((probe) => {\n if (pending !== awaited || !awaited) return;\n if (probe === 'over') fail();\n else armIdleCheck();\n });\n }, VOICE_ANSWER_IDLE_MS);\n };\n\n const answer = (text: string, others: VoiceAgentReply[]) => {\n settle();\n const shown = deps.present(text);\n const answered = answerTranscriptLines({ text: shown, others: others.map((o) => ({ ...o, text: deps.present(o.text) })) });\n lines = [...lines, ...answered.map((line) => ({ role: 'assistant' as const, ...line }))];\n const words = shown.trim() ? deps.speakable(shown) : '';\n if (!words.trim()) {\n // The addressed agent said nothing to read out.\n listen();\n return;\n }\n const current = new AbortController();\n speech = current;\n speaking = words;\n heard = '';\n said = '';\n setPhase('speaking');\n void deps\n .speak(words, current.signal)\n .catch(() => undefined)\n .then(() => {\n if (speech !== current) return;\n speech = null;\n speaking = '';\n if (phase === 'speaking') listen();\n });\n };\n\n const handleFrame = (frame: VoiceFrame) => {\n if (!pending || !armed) return;\n const verdict = classifyVoiceFrame(pending, frame);\n if (verdict.kind === 'ignore') return;\n if (verdict.kind === 'answer') answer(verdict.text, verdict.others);\n else if (verdict.kind === 'failed') fail();\n else armIdleCheck();\n };\n\n const ask = (text: string) => {\n const asked = ++question;\n silenceTimer = clear(silenceTimer);\n said = '';\n heard = '';\n lines = [...lines, { role: 'user', content: text }];\n pending = newPendingAnswer(deps.addressedAgentId());\n // The frames of a turn running before this question went out are not its answer.\n armed = false;\n setPhase('processing');\n armIdleCheck();\n const current = () => asked === question && pending !== null;\n deps\n .send(text, {\n onAccepted: () => {\n if (current()) armed = true;\n },\n onNotSent: () => {\n if (current()) fail();\n },\n onSteered: (turnId, unanswered) => {\n if (!current()) return;\n armed = true;\n // Stored with no turn named: none will answer it when the backend\n // says so; otherwise one may, and the idle check settles the wait.\n handleFrame(\n turnId\n ? { type: 'folded_into_active_turn', folded: true, turn_id: turnId }\n : { type: 'folded_into_active_turn', ...(unanswered ? { folded: false } : {}) },\n );\n },\n })\n .then(\n (outcome) => {\n if (current() && (outcome === 'dropped' || outcome === 'failed')) fail();\n },\n () => {\n if (current()) fail();\n },\n );\n };\n\n const cancelQuestion = () => {\n question += 1;\n settle();\n };\n\n recognition.continuous = true;\n recognition.interimResults = true;\n if (deps.lang) recognition.lang = deps.lang;\n recognition.onresult = (event) => {\n recognitionFailures = 0;\n if (muted || (phase !== 'listening' && phase !== 'speaking')) return;\n let finalText = '';\n let interim = '';\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 interim += result[0].transcript;\n }\n if (phase === 'speaking') {\n // The call's own voice comes back through the microphone; the user\n // talking over it takes the turn back.\n if (!isBargeIn(`${finalText} ${interim}`, speaking)) return;\n stopSpeaking();\n said = '';\n phase = 'listening';\n }\n if (finalText.trim()) said = [said, finalText.trim()].filter(Boolean).join(' ');\n heard = [said, interim.trim()].filter(Boolean).join(' ');\n silenceTimer = clear(silenceTimer);\n if (said) {\n silenceTimer = setTimer(() => {\n silenceTimer = null;\n if (phase === 'listening' && !muted && said.trim()) ask(said.trim());\n }, VOICE_SILENCE_MS);\n }\n emit();\n };\n recognition.onerror = (event) => {\n recognitionFailed = true;\n if (!RECOGNITION_ROUTINE_ERRORS.has(event?.error ?? '')) recognitionFailures += 1;\n };\n recognition.onend = () => {\n recognitionRunning = false;\n const failed = recognitionFailed;\n recognitionFailed = false;\n if (!wantsRecognition()) return;\n if (recognitionFailures >= RECOGNITION_MAX_FAILURES) {\n // No microphone, permission refused, offline, a language it lacks:\n // muted rather than retried in a loop; unmuting tries again.\n recognitionFailures = 0;\n muted = true;\n emit();\n return;\n }\n if (!failed) {\n syncRecognition();\n return;\n }\n restartTimer = setTimer(() => {\n restartTimer = null;\n syncRecognition();\n }, RECOGNITION_RETRY_MS);\n };\n\n return {\n start: listen,\n end: () => {\n if (phase === 'ended') return;\n cancelQuestion();\n stopSpeaking();\n silenceTimer = clear(silenceTimer);\n retryTimer = clear(retryTimer);\n restartTimer = clear(restartTimer);\n phase = 'ended';\n recognition.onresult = null;\n recognition.onerror = null;\n recognition.onend = null;\n if (recognitionRunning) {\n try {\n recognition.abort();\n } catch {\n // Already ended.\n }\n }\n recognitionRunning = false;\n emit();\n },\n frame: (frame, relayed) => handleFrame({ ...frame, relayed: relayed || frame.relayed === true }),\n interrupt: () => {\n if (phase === 'speaking') {\n stopSpeaking();\n listen();\n } else if (phase === 'processing') {\n cancelQuestion();\n deps.stopTurn();\n listen();\n }\n },\n cancel: () => {\n if (phase !== 'processing' && phase !== 'speaking') return;\n cancelQuestion();\n stopSpeaking();\n listen();\n },\n toggleMute: () => {\n if (phase === 'ended') return;\n muted = !muted;\n recognitionFailures = 0;\n if (muted) {\n silenceTimer = clear(silenceTimer);\n said = '';\n heard = '';\n } else if (phase === 'speaking') {\n // Unmuting is a way to take the turn back too.\n stopSpeaking();\n listen();\n return;\n }\n syncRecognition();\n emit();\n },\n };\n}\n","import { useCallback, useEffect, useInsertionEffect, useRef, useState } from 'react';\nimport { chunkSpeech, speechAhead, type TtsRoute } from '../utils/speech';\nimport { getSpeechRecognition } from './useDictation';\nimport { createVoiceCall, type VoiceCallController, type VoiceCallDeps, type VoiceCallSnapshot } from './voiceCallController';\nimport { probeTurn } from './voiceTurn';\nimport { usePanelFetch } from './usePanelRequests';\nimport { askHost } from '../utils/askHost';\n\n/** Longest piece of an answer spoken at once: one plays while the next is made. */\nconst VOICE_CHUNK_LENGTH = 200;\n\n/** Whether this browser can hold a voice call: it needs speech recognition. */\nexport const voiceCallSupported = (): boolean => getSpeechRecognition() !== null;\n\ninterface UseVoiceCallOptions {\n /** Sends a question through the panel, as the user. */\n send: VoiceCallDeps['send'];\n /** Stops the turn running (the Stop button's). */\n stopTurn: () => void;\n /** Whether the conversation's turn still runs, for the idle check; `null` when it could not be read. */\n readTurnState?: () => Promise<{ running: boolean } | null>;\n addressedAgentId: () => string | null;\n speakable: (text: string) => string;\n present: (text: string) => string;\n /** The host's speech route; the browser's voice speaks without it, or when it fails. */\n tts?: TtsRoute;\n /** Language listened for and spoken in; the browser's own when unset. */\n locale?: string;\n}\n\n/** Play *blob*; resolves once it ended or *signal* aborted it (`true`), or the browser could not play it (`false`). */\nfunction playAudio(blob: Blob, signal: AbortSignal): Promise<boolean> {\n return new Promise<boolean>((resolve) => {\n const url = URL.createObjectURL(blob);\n const audio = new Audio(url);\n const done = (played: boolean) => {\n audio.pause();\n URL.revokeObjectURL(url);\n signal.removeEventListener('abort', aborted);\n resolve(played);\n };\n const aborted = () => done(true);\n audio.onended = () => done(true);\n audio.onerror = () => done(false);\n signal.addEventListener('abort', aborted);\n audio.play().catch(() => done(false));\n });\n}\n\n/** Say *text* in the browser's voice; resolves at once without one. */\nfunction speakInBrowser(text: string, lang: string | undefined, signal: AbortSignal): Promise<void> {\n if (typeof window === 'undefined' || !window.speechSynthesis || typeof window.SpeechSynthesisUtterance !== 'function') return Promise.resolve();\n return new Promise<void>((resolve) => {\n const utterance = new SpeechSynthesisUtterance(text);\n if (lang) utterance.lang = lang;\n const done = () => {\n signal.removeEventListener('abort', stop);\n resolve();\n };\n const stop = () => {\n window.speechSynthesis.cancel();\n done();\n };\n utterance.onend = done;\n utterance.onerror = done;\n signal.addEventListener('abort', stop);\n window.speechSynthesis.speak(utterance);\n });\n}\n\n/**\n * A voice call over the panel: the browser listens, the panel sends, the\n * answer is spoken through the host's speech route in sentence-sized pieces,\n * played in order, the next one requested while one plays (`speechAhead`),\n * each piece the route fails in the browser's voice. `onFrame` must see every\n * frame of both streams.\n */\nexport function useVoiceCall(options: UseVoiceCallOptions) {\n const panelFetch = usePanelFetch();\n const optionsRef = useRef(options);\n useInsertionEffect(() => {\n optionsRef.current = options;\n });\n const [snapshot, setSnapshot] = useState<VoiceCallSnapshot | null>(null);\n const callRef = useRef<VoiceCallController | null>(null);\n\n const speak = useCallback(async (text: string, signal: AbortSignal) => {\n const { tts, locale } = optionsRef.current;\n const lang = locale || (typeof navigator !== 'undefined' ? navigator.language : undefined);\n const chunks = chunkSpeech(text, VOICE_CHUNK_LENGTH);\n const audio = tts ? speechAhead(panelFetch, tts, chunks, signal) : null;\n for (const chunk of chunks) {\n if (signal.aborted) return;\n const blob = audio ? ((await audio.next()).value ?? null) : null;\n if (signal.aborted) return;\n // A piece the route fails, or the browser cannot play, is said in the browser's voice.\n if (blob && (await playAudio(blob, signal))) continue;\n if (signal.aborted) return;\n await speakInBrowser(chunk, lang, signal);\n }\n }, []);\n\n const start = useCallback((): boolean => {\n if (callRef.current) return true;\n const Recognition = getSpeechRecognition();\n if (!Recognition) return false;\n const call = createVoiceCall({\n recognition: new Recognition(),\n lang: optionsRef.current.locale || (typeof navigator !== 'undefined' ? navigator.language : undefined),\n send: (text, hooks) => optionsRef.current.send(text, hooks),\n speak,\n // The host's rewriting that throws leaves the text as it is: the answer is\n // still shown and spoken, and the call goes on.\n speakable: (text) => askHost(optionsRef.current.speakable, text) ?? text,\n present: (text) => askHost(optionsRef.current.present, text) ?? text,\n // Without a way to read the turn, a turn silent for two minutes is taken as over.\n probe: () => {\n const read = optionsRef.current.readTurnState;\n return read ? probeTurn(read) : Promise.resolve('over');\n },\n stopTurn: () => optionsRef.current.stopTurn(),\n addressedAgentId: () => optionsRef.current.addressedAgentId(),\n // An ended call says nothing more.\n onChange: (next) => {\n if (callRef.current === call) setSnapshot(next);\n },\n });\n callRef.current = call;\n call.start();\n return true;\n }, [speak]);\n\n const end = useCallback(() => {\n const call = callRef.current;\n callRef.current = null;\n call?.end();\n setSnapshot(null);\n }, []);\n\n useEffect(() => end, [end]);\n\n return {\n /** The call on screen, or `null`. */\n snapshot,\n start,\n end,\n onFrame: useCallback((frame: Record<string, unknown>, relayed: boolean) => callRef.current?.frame(frame, relayed), []),\n interrupt: useCallback(() => callRef.current?.interrupt(), []),\n cancel: useCallback(() => callRef.current?.cancel(), []),\n toggleMute: useCallback(() => callRef.current?.toggleMute(), []),\n };\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 ChevronRightIcon = ({ 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 18 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 FolderIcon = ({ 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 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FolderInputIcon = ({ 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=\"M2 9V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-1\" />\n <path d=\"M2 13h10\" />\n <path d=\"m9 16 3-3-3-3\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FolderPlusIcon = ({ 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 10v6\" />\n <path d=\"M9 13h6\" />\n <path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\" />\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 MessageSquareIcon = ({ 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 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\" />\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 PencilIcon = ({ 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.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\" />\n <path d=\"m15 5 4 4\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const PhoneOffIcon = ({ 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=\"M10.1 13.9a14 14 0 0 0 3.732 2.668 1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2 18 18 0 0 1-12.728-5.272\" />\n <path d=\"M22 2 2 22\" />\n <path d=\"M4.76 13.582A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 .244.473\" />\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\nconst SPEAKER =\n 'M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z';\n\nexport const VolumeIcon = ({ 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={SPEAKER} />\n <path d=\"M16 9a5 5 0 0 1 0 6\" />\n <path d=\"M19.364 18.364a9 9 0 0 0 0-12.728\" />\n </svg>\n);\n\nexport const VolumeOffIcon = ({ 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={SPEAKER} />\n <path d=\"m22 9-6 6\" />\n <path d=\"m16 9 6 6\" />\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 { useState } from 'react';\nimport type { Translate } from '../types';\nimport { CheckIcon, CloseIcon, TrashIcon } from './icons';\n\ninterface ClearAllConversationsProps {\n /** Called once the user confirmed. */\n onClear: () => void;\n /** The row's layout where it stands. */\n className: string;\n /** The look of the confirm and cancel buttons where it stands. */\n iconButtonClass: string;\n t: Translate;\n}\n\n/** \"Clear all conversations\", asked again in place before anything is deleted. */\nexport function ClearAllConversations({ onClear, className, iconButtonClass, t }: ClearAllConversationsProps) {\n const [confirming, setConfirming] = useState(false);\n return (\n <div className={`flex items-center gap-1 ${className}`}>\n {confirming ? (\n <>\n <span className=\"flex-1 text-[0.7rem] text-red-600 dark:text-red-400\">{t('Delete all conversations?')}</span>\n <button\n type=\"button\"\n onClick={() => {\n setConfirming(false);\n onClear();\n }}\n aria-label={t('Delete all conversations')}\n className={`${iconButtonClass} hover:text-red-500`}\n >\n <CheckIcon size={13} />\n </button>\n <button type=\"button\" onClick={() => setConfirming(false)} aria-label={t('Cancel')} className={iconButtonClass}>\n <CloseIcon size={13} />\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n onClick={() => setConfirming(true)}\n className=\"flex items-center gap-1.5 rounded-md px-1 py-0.5 text-[0.7rem] text-gray-400 dark:text-white/40 hover:text-red-500 dark:hover:text-red-400 transition-colors\"\n >\n <TrashIcon size={12} />\n {t('Clear all conversations')}\n </button>\n )}\n </div>\n );\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","/**\n * Where a tooltip is drawn.\n *\n * It is centred above its anchor. Near the side of the panel (the time under a\n * message) it would run past it - into the host page, or off the screen - and\n * near the top it would run above it, into the host app's top bar. So its width\n * is capped to the room there is (a long label wraps instead), it is shifted back\n * inside, and it opens below its anchor when there is no room above.\n */\n\n/** Space between a tooltip and its anchor. */\nexport const ANCHOR_GAP = 4;\n\n/** Space kept between a tooltip and the side of the panel it would cross. */\nexport const EDGE_MARGIN = 4;\n\n/** A box in viewport coordinates, as `getBoundingClientRect` returns it. */\nexport interface Box {\n readonly top: number;\n readonly bottom: number;\n readonly left: number;\n readonly right: number;\n}\n\n/** A tooltip's anchor and the room it may take: the part of the panel inside the viewport, less the margins. */\nexport interface TooltipOpening {\n readonly top: number;\n readonly bottom: number;\n readonly center: number;\n readonly minTop: number;\n readonly minLeft: number;\n readonly maxRight: number;\n}\n\nexport interface TooltipPlacement {\n /** Drawn below its anchor instead of above. */\n readonly below: boolean;\n /** Horizontal offset from the anchor's centre that keeps it inside. */\n readonly shift: number;\n}\n\nexport function tooltipOpening(anchor: Box, panel: Box, viewportWidth: number): TooltipOpening {\n return {\n top: anchor.top,\n bottom: anchor.bottom,\n center: (anchor.left + anchor.right) / 2,\n minTop: Math.max(panel.top, 0),\n minLeft: Math.max(panel.left, 0) + EDGE_MARGIN,\n maxRight: Math.min(panel.right, viewportWidth) - EDGE_MARGIN,\n };\n}\n\n/** The widest a tooltip may be drawn: wider than that, it would cross the panel's side whatever its shift. */\nexport function tooltipMaxWidth(opening: TooltipOpening): number {\n return Math.max(opening.maxRight - opening.minLeft, 0);\n}\n\n/** Places a tooltip of the measured size, drawn no wider than `tooltipMaxWidth`. */\nexport function placeTooltip(opening: TooltipOpening, size: { readonly width: number; readonly height: number }): TooltipPlacement {\n const left = opening.center - size.width / 2;\n let shift = 0;\n if (left < opening.minLeft) shift = opening.minLeft - left;\n else if (left + size.width > opening.maxRight) shift = opening.maxRight - (left + size.width);\n return { below: opening.top - ANCHOR_GAP - size.height < opening.minTop, shift };\n}\n","import { useLayoutEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { findChatbotRoot } from '../utils';\nimport { ANCHOR_GAP, placeTooltip, tooltipMaxWidth, tooltipOpening, type TooltipOpening } from '../utils/tooltipPlacement';\n\ninterface TooltipProps {\n title: string;\n children: React.ReactElement;\n}\n\nexport const Tooltip = ({ title, children }: TooltipProps) => {\n const ref = useRef<HTMLSpanElement>(null);\n const tipRef = useRef<HTMLSpanElement>(null);\n const [opening, setOpening] = useState<TooltipOpening | null>(null);\n const [placement, setPlacement] = useState({ below: false, shift: 0 });\n\n // Its size is its text's, wrapped to the room there is: measured once shown,\n // before the browser paints it. The tooltip lives inside the panel's stacking\n // context (z-[1200] in sidebar mode), so anything drawn above the panel lands\n // in the host app's top-bar zone and is hidden whenever the host bar stacks\n // higher (e.g. OpenAEV's AppBar at theme.zIndex.drawer + 1).\n useLayoutEffect(() => {\n if (!opening || !tipRef.current) return;\n const { width, height } = tipRef.current.getBoundingClientRect();\n setPlacement(placeTooltip(opening, { width, height }));\n }, [opening, title]);\n\n if (!title) return children;\n\n const open = () => {\n if (!ref.current) return;\n const panel = findChatbotRoot(ref.current).getBoundingClientRect();\n setOpening(tooltipOpening(ref.current.getBoundingClientRect(), panel, window.innerWidth));\n };\n\n // A keyboard user reaches the control without hovering it, so its tooltip\n // opens on focus too - on keyboard focus only: a click also focuses, and the\n // hover has already shown the tooltip then.\n const handleFocus = (event: React.FocusEvent) => {\n const target = event.target as HTMLElement;\n if (typeof target.matches === 'function' && target.matches(':focus-visible')) open();\n };\n\n return (\n <span\n ref={ref}\n className=\"inline-flex\"\n onMouseEnter={open}\n onMouseLeave={() => setOpening(null)}\n onFocus={handleFocus}\n onBlur={() => setOpening(null)}\n >\n {children}\n {opening &&\n createPortal(\n <span\n ref={tipRef}\n className={`pointer-events-none fixed z-[10001] w-max -translate-x-1/2 ${placement.below ? '' : '-translate-y-full'} whitespace-normal break-words 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={{\n top: placement.below ? opening.bottom + ANCHOR_GAP : opening.top - ANCHOR_GAP,\n left: opening.center + placement.shift,\n maxWidth: tooltipMaxWidth(opening),\n }}\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, ChatPanelMode, Translate, XtmAgent } from '../types';\nimport { agentIconKind, groupAgents, timeAgo, translate } 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 { ClearAllConversations } from './ClearAllConversations';\nimport { Dropdown } from './Dropdown';\nimport { Spinner } from './Spinner';\nimport { Tooltip } from './Tooltip';\n\ninterface ChatHeaderProps {\n /** Inline mode has neither the view switcher nor the close button. */\n mode: ChatPanelMode;\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 /** Offers \"Clear all conversations\" under the history menu, confirmed first. */\n onClearAll?: () => void;\n /** The host's controls, before New chat. */\n actions?: React.ReactNode;\n t: Translate;\n}\n\n/** An agent's own icon when it has one the panel can draw, the panel's logo otherwise. */\nconst AgentIcon = ({ icon, fallback }: { icon: string | null | undefined; fallback: React.ReactNode }) => {\n const kind = agentIconKind(icon);\n if (kind === 'image') return <img src={icon!.trim()} alt=\"\" className=\"w-4 h-4 rounded-sm object-cover\" />;\n if (kind === 'emoji') return <span className=\"text-sm leading-none\">{icon!.trim()}</span>;\n return <span className=\"text-[var(--chat-accent)] [&>svg]:w-4 [&>svg]:h-4\">{fallback}</span>;\n};\n\n// Labels are thunks rather than bare strings so each key stays a literal call\n// on the host's translator in the source: a table of plain keys resolved later\n// through a variable works at runtime but is invisible to key extraction, which\n// is how a locale ends up missing exactly the strings nobody typed by hand.\nconst modeOptions: { mode: ChatMode; label: (t: Translate) => string; getIcon: (p: { size: number; className: string }) => React.ReactNode }[] = [\n { mode: 'floating', label: (t) => t('Floating'), getIcon: (p) => <FloatingIcon {...p} /> },\n { mode: 'sidebar', label: (t) => t('Sidebar'), getIcon: (p) => <SidebarIcon {...p} /> },\n { mode: 'fullscreen', label: (t) => t('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 onClearAll,\n actions,\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 max-w-full 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 shrink-0 items-center text-[var(--chat-accent)] [&>svg]:w-[18px] [&>svg]:h-[18px]\">{logoIcon}</span>\n <span className=\"min-w-0 truncate\">{agentName}</span>\n <ChevronDownIcon size={16} className=\"shrink-0 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 {translate(t, 'Transferred from {agent}', { agent: 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\">\n {t('Could not reach the assistant service. Check the connection and try again.')}\n </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 {groupAgents(filteredAgents).map(({ group, agents: groupMembers }) => (\n <div key={group ?? ''} role=\"group\" aria-label={group ?? undefined}>\n {group && (\n <span className=\"block px-4 pt-2 pb-0.5 text-[0.625rem] font-medium uppercase tracking-wide text-gray-400 dark:text-white/40\">\n {group}\n </span>\n )}\n {groupMembers.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 <AgentIcon icon={agent.icon} fallback={logoIcon} />\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 ))}\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 {onClearAll && conversations.length > 0 && (\n <ClearAllConversations\n onClear={() => {\n onHistoryMenuClose?.();\n onClearAll();\n }}\n className=\"px-3 pb-2\"\n iconButtonClass=\"p-1 rounded-md text-gray-400 dark:text-white/40 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n t={t}\n />\n )}\n </Dropdown>\n </>\n )}\n\n {actions}\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 {mode !== 'inline' && onModeChange && (\n <>\n <Tooltip title={t('Switch view')}>\n <button\n ref={modeAnchorRef}\n type=\"button\"\n aria-label={t('Switch view')}\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\">{opt.label(t)}</span>\n </button>\n ))}\n </div>\n </Dropdown>\n </>\n )}\n {mode !== 'inline' && onClose && (\n <>\n <Tooltip title={t('Close')}>\n <button\n type=\"button\"\n aria-label={t('Close')}\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 </>\n )}\n </div>\n );\n};\n","import { useEffect, useState } from 'react';\nimport {\n EMPTY_REFERENCE_SEARCH,\n REFERENCE_SEARCH_DEBOUNCE_MS,\n parseReferenceCandidates,\n referenceSearchUrl,\n type ReferenceSearchState,\n} from '../utils/conversationRefs';\nimport { usePanelFetch } from './usePanelRequests';\n\ninterface UseConversationReferenceSearchOptions {\n /** `conversationReferencesUrl(...)`: null while the feature is off. */\n url: string | null;\n /** The text typed after `@`, or null while no `@` is being typed. */\n query: string | null;\n /** The conversation written in, left out of the answer. */\n exclude?: string | null;\n requestHeaders?: Record<string, string>;\n}\n\n/**\n * The conversations the `@` menu offers for the text typed after `@`.\n *\n * Searched once the text has rested `REFERENCE_SEARCH_DEBOUNCE_MS`. A request\n * overtaken by the next keystroke is aborted, and its answer, should it land\n * anyway, is dropped: only the latest text is ever answered. The previous\n * answer stays until then, so typing never empties the menu.\n */\nexport function useConversationReferenceSearch({ url, query, exclude, requestHeaders }: UseConversationReferenceSearchOptions): ReferenceSearchState {\n const panelFetch = usePanelFetch();\n const [state, setState] = useState<ReferenceSearchState>(EMPTY_REFERENCE_SEARCH);\n\n useEffect(() => {\n if (!url || query === null) {\n // A menu opened later starts from nothing, not from an old answer.\n setState(EMPTY_REFERENCE_SEARCH);\n return;\n }\n let current = true;\n const controller = new AbortController();\n const timer = window.setTimeout(async () => {\n try {\n const res = await panelFetch(referenceSearchUrl(url, query, exclude), {\n credentials: 'include',\n headers: { ...(requestHeaders ?? {}) },\n signal: controller.signal,\n });\n const data = res.ok ? await res.json() : null;\n if (!current) return;\n setState(res.ok ? { query, items: parseReferenceCandidates(data), failed: false } : { query, items: [], failed: true });\n } catch {\n if (current) setState({ query, items: [], failed: true });\n }\n }, REFERENCE_SEARCH_DEBOUNCE_MS);\n return () => {\n current = false;\n window.clearTimeout(timer);\n controller.abort();\n };\n }, [url, query, exclude, requestHeaders]);\n\n return state;\n}\n","import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent, type RefObject } from 'react';\nimport type { ChatConversationRef, ChatConversationReferenceCandidate, XtmAgent } from '../types';\nimport { agentCandidates, type AgentMention } from '../utils/agentMentions';\nimport {\n insertConversationReference,\n refAsCandidate,\n referenceMenuView,\n rememberAnswer,\n rememberCandidates,\n referenceTrigger,\n referencedConversations,\n type ReferenceMenuView,\n type ReferenceTrigger,\n} from '../utils/conversationRefs';\nimport { useConversationReferenceSearch } from './useConversationReferenceSearch';\n\ninterface UseMentionMenuOptions {\n /** `conversationReferencesUrl(...)`: null while the feature is off. */\n url: string | null;\n requestHeaders?: Record<string, string>;\n /** The conversation written in: never offered. */\n conversationId?: string | null;\n text: string;\n onTextChange: (text: string) => void;\n textareaRef: RefObject<HTMLTextAreaElement | null>;\n /** The picks the composer holds, to tell when the message is full. */\n refs: ChatConversationRef[];\n /** Called with a pick before the text changes, so pruning sees it inserted. */\n onPick: (ref: ChatConversationRef) => void;\n /** The agents `@` can mention, above the conversations; omitted when mentions are off. */\n agentMentions?: { agents: XtmAgent[]; onPick: (mention: AgentMention) => void };\n}\n\n/** A row of the `@` menu. */\nexport type ReferenceMenuItem = { type: 'agent'; mention: AgentMention } | { type: 'conversation'; candidate: ChatConversationReferenceCandidate };\n\nconst sameTrigger = (a: ReferenceTrigger | null, b: ReferenceTrigger | null) =>\n a === b || (!!a && !!b && a.anchor === b.anchor && a.query === b.query);\n\n/**\n * The composer's `@` menu: what typing `@` offers, and how a pick lands in the\n * text.\n *\n * The menu follows the caret, read from the field after every change of the\n * text - typed, dictated, a template, the clear after a send - and after every\n * move of the caret the caller reports through `sync`. The field keeps the\n * focus throughout: the menu is driven from its keys, and a row is picked on\n * `mousedown`.\n */\nexport function useMentionMenu({\n url,\n requestHeaders,\n conversationId,\n text,\n onTextChange,\n textareaRef,\n refs,\n onPick,\n agentMentions,\n}: UseMentionMenuOptions) {\n const enabled = url !== null || !!agentMentions;\n const [trigger, setTrigger] = useState<ReferenceTrigger | null>(null);\n // Escape closes the menu for the `@` it was open on; a new `@` opens it again.\n const [dismissedAnchor, setDismissedAnchor] = useState<number | null>(null);\n const [activeIndex, setActiveIndex] = useState(0);\n const pendingCaretRef = useRef<number | null>(null);\n const listboxId = useId();\n const optionId = useCallback((index: number) => `${listboxId}-option-${index}`, [listboxId]);\n\n /** Read the caret: open, narrow or close the menu. */\n const sync = useCallback(() => {\n const el = textareaRef.current;\n const next =\n enabled && el && document.activeElement === el && el.selectionStart === el.selectionEnd\n ? referenceTrigger(el.value.slice(0, el.selectionStart))\n : null;\n setTrigger((prev) => (sameTrigger(prev, next) ? prev : next));\n }, [enabled, textareaRef]);\n\n const close = useCallback(() => setTrigger(null), []);\n\n // After a pick the caret goes after the inserted key, before `sync` reads it.\n useLayoutEffect(() => {\n const caret = pendingCaretRef.current;\n if (caret === null) return;\n pendingCaretRef.current = null;\n const el = textareaRef.current;\n if (!el) return;\n el.focus();\n el.setSelectionRange(caret, caret);\n }, [text, textareaRef]);\n\n useEffect(() => {\n sync();\n }, [text, sync]);\n\n useEffect(() => {\n if (!trigger) setDismissedAnchor(null);\n }, [trigger]);\n\n const search = useConversationReferenceSearch({ url, query: trigger?.query ?? null, exclude: conversationId, requestHeaders });\n\n // Every conversation the menu has offered, matched locally by key and title\n // (`referenceMenuView`'s `known`), with the draft's picks.\n // A settled answer also drops the remembered rows it should have listed\n // (deleted, unshared since); another conversation starts from nothing.\n const [seen, setSeen] = useState<ChatConversationReferenceCandidate[]>([]);\n useEffect(() => {\n setSeen([]);\n }, [conversationId]);\n useEffect(() => {\n setSeen((prev) => rememberAnswer(prev, search, conversationId));\n }, [search, conversationId]);\n const known = useMemo(() => rememberCandidates(refs.map(refAsCandidate), seen), [refs, seen]);\n\n // Every new answer starts on its first row.\n useEffect(() => {\n setActiveIndex(0);\n }, [search]);\n\n const view: ReferenceMenuView = referenceMenuView({\n trigger,\n dismissed: trigger !== null && trigger.anchor === dismissedAnchor,\n search,\n referenced: referencedConversations(refs, text).length,\n exclude: conversationId,\n known,\n });\n const dismissed = trigger !== null && trigger.anchor === dismissedAnchor;\n const agentRows = useMemo(\n () => (agentMentions && trigger && !dismissed ? agentCandidates(agentMentions.agents, trigger.query) : []),\n [agentMentions, trigger, dismissed],\n );\n const items: ReferenceMenuItem[] = [\n ...agentRows.map((mention) => ({ type: 'agent' as const, mention })),\n ...(url !== null && view.kind === 'results' ? view.items.map((candidate) => ({ type: 'conversation' as const, candidate })) : []),\n ];\n const active = Math.min(activeIndex, Math.max(items.length - 1, 0));\n\n const pick = (item: ReferenceMenuItem) => {\n if (!trigger) return;\n const key = item.type === 'agent' ? item.mention.key : item.candidate.key;\n const inserted = insertConversationReference(text, trigger, key);\n if (item.type === 'agent') agentMentions?.onPick(item.mention);\n else onPick({ conversationId: item.candidate.id, title: item.candidate.title, key, isOwn: item.candidate.isOwn });\n pendingCaretRef.current = inserted.caret;\n onTextChange(inserted.text);\n setTrigger(null);\n };\n\n /** The menu's keys; true when the key was the menu's to handle. */\n const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): boolean => {\n if ((view.kind === 'closed' && items.length === 0) || !trigger || e.nativeEvent.isComposing) return false;\n if (e.key === 'Escape') {\n e.preventDefault();\n setDismissedAnchor(trigger.anchor);\n return true;\n }\n if (items.length === 0) return false;\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n e.preventDefault();\n const step = e.key === 'ArrowDown' ? 1 : -1;\n setActiveIndex((active + step + items.length) % items.length);\n return true;\n }\n if ((e.key === 'Enter' || e.key === 'Tab') && !e.shiftKey) {\n e.preventDefault();\n pick(items[active]);\n return true;\n }\n return false;\n };\n\n // The field stays the focus, so the list is announced through it.\n const textareaAria = enabled\n ? {\n 'aria-autocomplete': 'list' as const,\n 'aria-controls': items.length > 0 ? listboxId : undefined,\n 'aria-activedescendant': items.length > 0 ? optionId(active) : undefined,\n }\n : {};\n\n // Without conversation references, an agent-only menu: no conversation states to say.\n const shownView: ReferenceMenuView = url === null ? { kind: 'closed' } : view;\n return { view: shownView, items, activeIndex: active, setActiveIndex, listboxId, optionId, sync, close, pick, onKeyDown, textareaAria };\n}\n","import { useRef, useState } from 'react';\nimport type { ChatContextBreakdown, ChatContextUsage, Translate } from '../types';\nimport { compactCount, translate } from '../utils';\nimport { Dropdown } from './Dropdown';\nimport { Tooltip } from './Tooltip';\n\ninterface ContextUsageIndicatorProps {\n usage: ChatContextUsage;\n t: Translate;\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: (t: Translate) => string; color: string }> = [\n { field: 'system', label: (t) => t('System prompt'), color: '#9ca3af' },\n { field: 'tools', label: (t) => t('Tool definitions'), color: '#a78bfa' },\n { field: 'dynamicTools', label: (t) => t('MCP & dynamic tools'), color: '#f0abfc' },\n { field: 'summary', label: (t) => t('Summarized conversation'), color: '#fb7185' },\n { field: 'toolResults', label: (t) => t('Tool results'), color: '#34d399' },\n { field: 'conversation', label: (t) => t('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 tokens = translate(t, '{counts} tokens', { counts });\n // Separator only — nothing to translate, so no key of its own: both halves\n // are already whole translated readouts.\n const summary = `${headline} · ${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 cx={SIZE / 2} cy={SIZE / 2} r={RADIUS} fill=\"none\" strokeWidth={STROKE} className=\"stroke-gray-200 dark:stroke-white/15\" />\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 ? '' : translate(t, '{summary} — click for details', { summary })}>\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}`}>{translate(t, '{percent}% full', { 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\">~{tokens}</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\">{row.label(t)}</span>\n <span className=\"ml-auto text-[0.7rem] tabular-nums text-gray-500 dark:text-white/40 shrink-0\">\n {compactCount(breakdown?.[row.field] ?? 0)}\n </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 { useLayoutEffect, useRef } from 'react';\nimport type { ReferenceMenuItem } from '../hooks/useMentionMenu';\nimport { translate } from '../utils';\nimport { MAX_REFERENCED_CONVERSATIONS, type ReferenceMenuView } from '../utils/conversationRefs';\nimport { BotIcon, MessageSquareIcon } from './icons';\n\ninterface MentionMenuProps {\n view: ReferenceMenuView;\n /** The rows, agents first: what the arrows walk through. */\n items: ReferenceMenuItem[];\n activeIndex: number;\n listboxId: string;\n optionId: (index: number) => string;\n onHover: (index: number) => void;\n onPick: (item: ReferenceMenuItem) => void;\n t: (key: string) => string;\n}\n\n/**\n * The composer's `@` menu, driven by `useMentionMenu`: the agents a message\n * can mention and the conversations it can reference, opened above the field. The field\n * keeps the focus - the rows are options it points at, picked on `mousedown`\n * so a click never takes the focus away from the text being written.\n */\nexport const MentionMenu = ({ view, items, activeIndex, listboxId, optionId, onHover, onPick, t }: MentionMenuProps) => {\n const listRef = useRef<HTMLDivElement>(null);\n\n // The row the arrows reached stays in view in a list longer than the menu.\n useLayoutEffect(() => {\n listRef.current?.querySelector<HTMLElement>('[aria-selected=\"true\"]')?.scrollIntoView?.({ block: 'nearest' });\n }, [activeIndex, view.kind]);\n\n if (view.kind === 'closed' && items.length === 0) return null;\n const hasAgents = items.some((item) => item.type === 'agent');\n // Agents come first, under their own heading; conversations follow under theirs.\n const heading = hasAgents ? t('Mention an agent') : t('Reference a conversation');\n\n return (\n <div className=\"absolute left-0 right-0 bottom-full mb-1.5 z-20 rounded-[10px] overflow-hidden border border-gray-200 dark:border-white/10 bg-white dark:bg-[#2a2a3e] shadow-xl\">\n <span className=\"block px-4 pt-3 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">{heading}</span>\n\n {items.length > 0 && (\n <div ref={listRef} id={listboxId} role=\"listbox\" aria-label={heading} className=\"max-h-[240px] overflow-y-auto pb-1 filigran-chat-scrollable\">\n {items.map((item, i) => {\n const active = i === activeIndex;\n const startsConversations = item.type === 'conversation' && hasAgents && items[i - 1]?.type === 'agent';\n return (\n <div key={item.type === 'agent' ? `agent-${item.mention.agentId}` : item.candidate.id}>\n {startsConversations && (\n <span className=\"block px-4 pt-2 pb-1 text-[0.68rem] tracking-[1px] uppercase text-gray-400 dark:text-white/40\">\n {t('Reference a conversation')}\n </span>\n )}\n <div\n id={optionId(i)}\n role=\"option\"\n aria-selected={active}\n onMouseDown={(e) => {\n e.preventDefault();\n onPick(item);\n }}\n // On a move, not on enter: a menu opening under a still pointer\n // must not take the selection off its first row.\n onMouseMove={() => {\n if (!active) onHover(i);\n }}\n className={`flex items-center gap-2.5 px-4 py-1.5 cursor-pointer transition-colors ${active ? 'bg-gray-100 dark:bg-white/10' : ''}`}\n >\n <span className=\"shrink-0 text-[var(--chat-accent)]\">\n {item.type === 'agent' ? <BotIcon size={14} /> : <MessageSquareIcon size={14} />}\n </span>\n {item.type === 'agent' ? (\n <span className=\"flex flex-col min-w-0 flex-1\">\n <span className=\"text-[0.8125rem] text-gray-900 dark:text-white truncate\">{item.mention.name}</span>\n <span className=\"text-[0.7rem] text-gray-500 dark:text-white/40 truncate\">@{item.mention.key}</span>\n </span>\n ) : (\n <span className=\"flex flex-col min-w-0 flex-1\">\n <span className=\"text-[0.8125rem] text-gray-900 dark:text-white truncate\">\n {item.candidate.title || t('Untitled conversation')}\n </span>\n <span className=\"text-[0.7rem] text-gray-500 dark:text-white/40 truncate\">\n @{item.candidate.key}\n {!item.candidate.isOwn && ` · ${t('Shared with you')}`}\n </span>\n </span>\n )}\n </div>\n </div>\n );\n })}\n </div>\n )}\n\n {view.kind === 'empty' && items.length === 0 && (\n <div role=\"status\" className=\"px-4 pt-1 pb-3 text-[0.75rem] text-gray-400 dark:text-white/40\">\n {t('No conversation matches')}\n </div>\n )}\n\n {view.kind === 'full' && (\n <div role=\"status\" className=\"px-4 pt-1 pb-3 text-[0.75rem] text-gray-400 dark:text-white/40\">\n {translate(t, 'A message references at most {count} conversations', { count: MAX_REFERENCED_CONVERSATIONS })}\n </div>\n )}\n </div>\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, translate } from '../utils';\nimport { quotaPeriodLabel } from '../utils/quota';\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 } = quota;\n const period = quotaPeriodLabel(quota.period, t);\n // One allowance for the whole platform: say so, or a user reads colleagues'\n // consumption as their own.\n const withScope = (title: string) => (quota.scope === 'global' ? translate(t, '{quota} (shared across all users)', { quota: title }) : title);\n\n // No ceiling: report consumption without implying a limit that isn't there.\n if (limit === null) {\n const title = withScope(period ? translate(t, 'Usage · {period}', { period }) : t('Usage'));\n return (\n <Tooltip title={title}>\n <span className=\"text-[0.68rem] tabular-nums text-gray-400 dark:text-white/30\" role=\"img\" aria-label={`${title} ${compact(used)}`}>\n {compact(used)}\n </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 headline = exhausted ? t('Quota reached') : t('Quota');\n const title = withScope(period ? `${headline} · ${period}` : headline);\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 { useLayoutEffect, useMemo, useRef, type KeyboardEvent } from 'react';\nimport type { ChatContextUsage, ChatConversationRef, ChatFile, ChatPanelMode, ChatPromptTemplate, ChatQuotaStatus, XtmAgent } from '../types';\nimport { mentionedAgentIds, type AgentMention } from '../utils/agentMentions';\nimport { AttachFileIcon, FileIcon, MicIcon, MicOffIcon, SendIcon, StopCircleIcon } from './icons';\nimport { useMentionMenu } from '../hooks/useMentionMenu';\nimport { useDictation } from '../hooks/useDictation';\nimport { referencedConversations } from '../utils/conversationRefs';\nimport { hasFailedUpload } from '../utils/failedUpload';\nimport { ContextUsageIndicator } from './ContextUsageIndicator';\nimport { MentionMenu } from './MentionMenu';\nimport { PromptPicker } from './PromptPicker';\nimport { QuotaIndicator } from './QuotaIndicator';\nimport { Tooltip } from './Tooltip';\n\ninterface ChatInputProps {\n /** The host's content above the composer (`composerHeader`). */\n header?: React.ReactNode;\n /** Uploaded files may steer the running turn. */\n steerAttachments?: boolean;\n /** A message that cannot steer is held until the turn ends. */\n canHold?: boolean;\n /** The message held, shown (in the words its bubble will show) with a way to take it back. */\n heldMessage?: { text: string; displayText?: string } | null;\n /** What the backend said when it refused a steer or a Stop. */\n notice?: string | null;\n onCancelHeld?: () => void;\n inputValue: string;\n onInputChange: (value: string) => void;\n onSend: () => void;\n onStop: () => void;\n isLoading: boolean;\n /** No turn on screen, yet one holds the conversation (winding down, or let go of with the event stream): a send now is held. */\n sendsHeld?: 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?: ChatPanelMode;\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 * The `@` conversation menu (`apiEndpoints.conversationReferences`), absent\n * while the feature is off: `@` is then plain text.\n */\n conversationReferences?: {\n /** `conversationReferencesUrl(...)`. */\n url: string;\n requestHeaders?: Record<string, string>;\n /** The conversation written in: never offered. */\n conversationId?: string | null;\n /** The picks the composer holds (see `useChat`). */\n refs: ChatConversationRef[];\n onPick: (ref: ChatConversationRef) => void;\n };\n /** The agents `@` mentions, and the mentions the composer holds; absent while mentions are off. */\n agentMentions?: { agents: XtmAgent[]; mentions: AgentMention[]; onPick: (mention: AgentMention) => void };\n}\n\nconst MAX_TEXTAREA_HEIGHT = 120;\nconst NO_REFS: ChatConversationRef[] = [];\n\nexport const ChatInput = ({\n header,\n steerAttachments = false,\n canHold = false,\n heldMessage,\n notice,\n onCancelHeld,\n inputValue,\n onInputChange,\n onSend,\n onStop,\n isLoading,\n sendsHeld = false,\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 conversationReferences,\n agentMentions,\n}: ChatInputProps) => {\n const fileInputRef = useRef<HTMLInputElement>(null);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const mentionAgents = agentMentions?.agents;\n const onPickMention = agentMentions?.onPick;\n const agentMentionOptions = useMemo(\n () => (mentionAgents && onPickMention ? { agents: mentionAgents, onPick: onPickMention } : undefined),\n [mentionAgents, onPickMention],\n );\n const mentionMenu = useMentionMenu({\n url: conversationReferences?.url ?? null,\n requestHeaders: conversationReferences?.requestHeaders,\n conversationId: conversationReferences?.conversationId,\n text: inputValue,\n onTextChange: onInputChange,\n textareaRef,\n refs: conversationReferences?.refs ?? NO_REFS,\n onPick: (ref) => conversationReferences?.onPick(ref),\n agentMentions: agentMentionOptions,\n });\n\n // A dictated phrase lands at the end of the draft while the focus is on the\n // mic, so nothing scrolls the field to it once the text outgrows the cap.\n const revealEndRef = useRef(false);\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 revealEndRef.current = true;\n onInputChange(inputValue.trim() ? `${inputValue.trimEnd()} ${finalText}` : finalText);\n });\n\n // Sized from the value rather than from the textarea's own change event:\n // dictation, template picks, draft restores and the clear after a send all\n // change the text without one.\n useLayoutEffect(() => {\n const el = textareaRef.current;\n if (!el) return;\n el.style.height = 'auto';\n el.style.height = `${Math.min(el.scrollHeight, MAX_TEXTAREA_HEIGHT)}px`;\n if (revealEndRef.current) {\n revealEndRef.current = false;\n el.scrollTop = el.scrollHeight;\n }\n }, [inputValue]);\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 // The toolbar row costs vertical space, so it only exists when something\n // actually occupies it. Dictation is not among them: it acts on the text\n // field, so it lives inside it, next to Send.\n const hasToolbar = Boolean((prompts && prompts.length > 0) || quota || contextUsage || composerToolbar);\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 hasAttachments = isFileManagementEnabled && attachedFiles.length > 0;\n // A file that failed to upload keeps the message from going while a turn\n // holds the conversation: the user removes it, or sends once the turn ends.\n const failedFileWaits = isFileManagementEnabled && (isLoading || sendsHeld) && hasFailedUpload(attachedFiles);\n const canSend = hasContent && !hasFilesUploading && !failedFileWaits;\n // A steer reaches the running loop as text alone, so the agent would never\n // read a conversation it references: such a message waits, like a file.\n // A mention routes the message to another agent: it waits for the turn too.\n const waitsForTurn =\n (Boolean(conversationReferences) && referencedConversations(conversationReferences?.refs ?? NO_REFS, inputValue).length > 0) ||\n (Boolean(agentMentions) && mentionedAgentIds(agentMentions?.mentions ?? [], inputValue).length > 0);\n // What the composer holds can steer the running agent: text, and\n // attachments only with steerAttachments. The accent Send button shows NEXT\n // to Stop while there is something to send, its files uploaded.\n const steerable = isLoading && canSteer && !waitsForTurn && (!hasAttachments || steerAttachments);\n const showSteerSend = steerable && !hasFilesUploading && !failedFileWaits && (hasAttachments || Boolean(inputValue.trim()));\n // What cannot steer waits for the turn to end, one message at a time.\n const showHoldSend = isLoading && canHold && !showSteerSend && !heldMessage && Boolean(canSend);\n\n // Every way of sending goes through here, and only when the matching button\n // would be enabled: Enter must not send while files upload, nor end a\n // dictation that nothing was sent from. Dictation is cancelled, not stopped:\n // a phrase still being recognised would land in the composer just emptied.\n const send = () => {\n if (!(isLoading ? showSteerSend || showHoldSend : canSend)) return;\n dictation.cancel();\n onSend();\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n // The menu's keys first (arrows, Enter / Tab, Escape) while it is open.\n if (mentionMenu.onKeyDown(e)) return;\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n if (e.key === 'Escape' && isLoading) {\n e.preventDefault();\n onStop();\n }\n };\n\n const footerText = showHoldSend\n ? t('Enter to send when the answer ends · Esc to stop')\n : failedFileWaits\n ? t('A file failed to upload: remove it, or send once the answer ends')\n : steerable\n ? t('Enter to send now · Esc to stop')\n : isLoading && hasAttachments\n ? t('Attachments wait for the current response')\n : isLoading && waitsForTurn\n ? t('This message waits 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 data-testid=\"filigran-chat-composer\"\n >\n {header && <div className=\"mb-2\">{header}</div>}\n {notice && (\n <div role=\"alert\" data-testid=\"filigran-chat-composer-notice\" className=\"mb-2 text-[0.75rem] text-red-500 dark:text-red-400\">\n {notice}\n </div>\n )}\n {heldMessage && (\n <div className=\"mb-2 flex items-center gap-2 rounded-lg border border-dashed border-[var(--chat-accent-40)] px-2.5 py-1.5 text-[0.75rem]\">\n <span className=\"shrink-0 text-gray-500 dark:text-white/50\">{t('Sends when the answer ends:')}</span>\n <span className=\"min-w-0 flex-1 truncate text-gray-800 dark:text-white/80\">\n {heldMessage.displayText ?? (heldMessage.text || t('Attached files'))}\n </span>\n {onCancelHeld && (\n <button type=\"button\" onClick={onCancelHeld} className=\"shrink-0 text-gray-400 dark:text-white/40 hover:text-red-500\">\n {t('Cancel')}\n </button>\n )}\n </div>\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 {/* items-end keeps the buttons on the last line once the field grows.\n `relative` anchors the @ menu, which opens above the field. */}\n <div\n className={`relative flex items-end border rounded-xl px-2 py-1 transition-colors ${\n dictation.listening ? 'border-red-500/50' : 'border-gray-200 dark:border-white/10 focus-within:border-[var(--chat-accent)]'\n } ${isLoading ? 'filigran-chat-active-border' : ''}`}\n >\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 aria-label={t('Attach files')}\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={dictation.listening ? t('Listening...') : t('Ask a question...')}\n value={inputValue}\n onChange={(e) => onInputChange(e.target.value)}\n onKeyDown={handleKeyDown}\n // The caret moved without the text changing: the @ menu follows it.\n onKeyUp={mentionMenu.sync}\n onClick={mentionMenu.sync}\n onFocus={mentionMenu.sync}\n onBlur={mentionMenu.close}\n {...mentionMenu.textareaAria}\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: MAX_TEXTAREA_HEIGHT }}\n />\n <MentionMenu\n view={mentionMenu.view}\n items={mentionMenu.items}\n activeIndex={mentionMenu.activeIndex}\n listboxId={mentionMenu.listboxId}\n optionId={mentionMenu.optionId}\n onHover={mentionMenu.setActiveIndex}\n onPick={mentionMenu.pick}\n t={t}\n />\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={t('Dictate a message')}\n aria-pressed={dictation.listening}\n className={`relative w-8 h-8 flex items-center justify-center shrink-0 rounded-lg mr-0.5 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={18} /> : <MicIcon size={18} />}\n {dictation.listening && <span className=\"absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-red-500 motion-safe:animate-pulse\" />}\n </button>\n </Tooltip>\n )}\n {showHoldSend && (\n <Tooltip title={t('Send when the answer ends')}>\n <button\n type=\"button\"\n onClick={send}\n aria-label={t('Send when the answer ends')}\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 {showSteerSend && (\n <Tooltip title={t('Send now')}>\n <button\n type=\"button\"\n onClick={send}\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 : send}\n aria-label={isLoading ? t('Stop generating') : t('Send')}\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 {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 {/* Words heard but not yet final take the footer's place, so the preview\n never shifts the composer's height. */}\n {dictation.interim ? (\n <p className=\"flex items-center justify-center gap-1.5 text-[0.65rem] italic text-red-500 dark:text-red-400 mt-1.5\">\n <span className=\"w-1.5 h-1.5 shrink-0 rounded-full bg-red-500 motion-safe:animate-pulse\" />\n <span className=\"truncate\">{dictation.interim}</span>\n </p>\n ) : (\n <p data-testid=\"filigran-chat-disclaimer\" className=\"text-center text-[0.65rem] text-gray-400 dark:text-white/30 mt-1.5 opacity-70\">\n {footerText}\n </p>\n )}\n </div>\n );\n};\n","import { useCallback, useInsertionEffect, useRef, useState } from 'react';\nimport type { ChatMessage, MessageFeedback, Translate } from '../types';\nimport { normalizeFeedbackComment, ratingOf, submitFeedback } from '../utils/feedback';\nimport { usePanelFetch } from './usePanelRequests';\n\n/** What the user did to one answer's rating in this session. */\nexport interface FeedbackEntry {\n /** Identifies the last change: a request answered after a newer one is ignored. */\n seq: number;\n value: MessageFeedback | null;\n comment: string | null;\n /** A request is on its way; the thumbs wait for it. */\n pending: boolean;\n /** The translated reason the last request failed, shown under the answer. */\n error: string | null;\n /** The optional comment asked for after a thumbs down. */\n commentOpen: boolean;\n}\n\n/**\n * The key an answer's rating is kept under. Not `id` alone: a restore renames\n * the rows a turn wrote, and one runs right after the first turn of a new\n * conversation - the persisted id is what stays the same.\n */\nexport const feedbackKeyOf = (msg: ChatMessage): string => msg.serverId ?? msg.id;\n\ninterface Options {\n /** `{apiBaseUrl}{feedback}` when the panel stores ratings itself (`feedbackBaseUrl`). */\n feedbackUrl?: string | null;\n conversationId?: string | null;\n requestHeaders?: Record<string, string>;\n onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;\n t: Translate;\n}\n\n/**\n * The thumbs of every answer in the transcript. A rating shows at once and is\n * taken back if the backend refuses it; the callbacks keep their identity, so\n * the memoized rows do not re-render when the panel does.\n *\n * Every action is handed the answer's current entry by its row: a click is a\n * discrete event, so the row has rendered the previous one's outcome by then.\n */\nexport function useMessageFeedback({ feedbackUrl, conversationId, requestHeaders, onMessageFeedback, t }: Options) {\n const panelFetch = usePanelFetch();\n const [state, setState] = useState<{ scope: string | null | undefined; entries: Record<string, FeedbackEntry> }>(() => ({\n scope: conversationId,\n entries: {},\n }));\n // Ratings belong to one conversation: another one starts from none, and a\n // request of the previous one answering late finds no entry to update.\n let entries = state.entries;\n if (state.scope !== conversationId) {\n entries = {};\n setState({ scope: conversationId, entries });\n }\n\n const latest = useRef({ feedbackUrl, conversationId, requestHeaders, onMessageFeedback, t });\n useInsertionEffect(() => {\n latest.current = { feedbackUrl, conversationId, requestHeaders, onMessageFeedback, t };\n });\n const seqRef = useRef(0);\n\n const put = useCallback((key: string, entry: FeedbackEntry) => setState((s) => ({ ...s, entries: { ...s.entries, [key]: entry } })), []);\n const patch = useCallback(\n (key: string, seq: number, change: (entry: FeedbackEntry) => FeedbackEntry) =>\n setState((s) => {\n const entry = s.entries[key];\n if (!entry || entry.seq !== seq) return s;\n return { ...s, entries: { ...s.entries, [key]: change(entry) } };\n }),\n [],\n );\n\n const rate = useCallback(\n async (msg: ChatMessage, next: MessageFeedback | null, current: FeedbackEntry | undefined) => {\n const { feedbackUrl: url, conversationId: convId, requestHeaders: headers, onMessageFeedback: notify, t: tr } = latest.current;\n const key = feedbackKeyOf(msg);\n const seq = ++seqRef.current;\n if (!url) {\n // The host stores it: nothing to wait for.\n put(key, { seq, value: next, comment: null, pending: false, error: null, commentOpen: false });\n notify?.(key, next, msg);\n return;\n }\n if (!convId || !msg.serverId) return;\n put(key, { seq, value: next, comment: null, pending: true, error: null, commentOpen: false });\n const stored = await submitFeedback(panelFetch, url, convId, msg.serverId, next ? { rating: ratingOf(next), comment: null } : null, headers);\n if (!stored) {\n const error = tr(next ? 'Could not submit feedback.' : 'Could not retract feedback.');\n patch(key, seq, () =>\n current\n ? { ...current, seq, pending: false, commentOpen: false, error }\n : { seq, value: null, comment: null, pending: false, error, commentOpen: false },\n );\n return;\n }\n // A thumbs down is worth a word on what went wrong - asked once it is\n // stored, so skipping the question loses nothing.\n patch(key, seq, (entry) => ({ ...entry, pending: false, commentOpen: next === 'down' }));\n latest.current.onMessageFeedback?.(key, next, msg);\n },\n [put, patch],\n );\n\n const saveComment = useCallback(\n async (msg: ChatMessage, text: string, current: FeedbackEntry) => {\n const { feedbackUrl: url, conversationId: convId, requestHeaders: headers, t: tr } = latest.current;\n const key = feedbackKeyOf(msg);\n const comment = normalizeFeedbackComment(text);\n if (!url || !convId || !msg.serverId || !comment) {\n put(key, { ...current, commentOpen: false, error: null });\n return;\n }\n const seq = ++seqRef.current;\n put(key, { ...current, seq, pending: true, error: null });\n const stored = await submitFeedback(panelFetch, url, convId, msg.serverId, { rating: 'negative', comment }, headers);\n patch(key, seq, (entry) =>\n stored\n ? { ...entry, comment, pending: false, commentOpen: false }\n : // The question stays open with what was typed, to try again.\n { ...entry, pending: false, error: tr('Could not submit feedback.') },\n );\n },\n [put, patch],\n );\n\n const skipComment = useCallback(\n (msg: ChatMessage, current: FeedbackEntry) => put(feedbackKeyOf(msg), { ...current, commentOpen: false, error: null }),\n [put],\n );\n\n return { entries, rate, saveComment, skipComment };\n}\n","import { useCallback, useEffect, useInsertionEffect, useRef, useState } from 'react';\nimport { chunkSpeech, createSpeechController, speakableText, speechAhead, type SpeechController, type TtsRoute } from '../utils/speech';\nimport { usePanelFetch } from './usePanelRequests';\n\nconst speechSupported = (): boolean =>\n typeof window !== 'undefined' && !!window.speechSynthesis && typeof window.SpeechSynthesisUtterance === 'function';\n\n/** The most text one request to the host's speech route carries; a longer answer takes several. */\nconst TTS_MAX_LENGTH = 4096;\n\n/**\n * Reads answers aloud, one at a time: through the host's speech route when it\n * names one, falling back to the browser's speech synthesis, in `locale` (the\n * browser's voice when unset). `supported` is false with neither: the panel\n * then offers no button at all.\n *\n * Whatever is being read stops when the transcript unmounts.\n */\nexport function useSpeechReader(locale?: string, tts?: TtsRoute) {\n const panelFetch = usePanelFetch();\n const [browserSupported] = useState(speechSupported);\n const supported = browserSupported || !!tts;\n const [speakingId, setSpeakingId] = useState<string | null>(null);\n const controllerRef = useRef<SpeechController | null>(null);\n const localeRef = useRef(locale);\n const ttsRef = useRef(tts);\n useInsertionEffect(() => {\n localeRef.current = locale;\n ttsRef.current = tts;\n });\n // What the host's route is playing, or fetching.\n const playbackRef = useRef<{ id: string; audio?: HTMLAudioElement; url?: string; abort: AbortController } | null>(null);\n\n const stopPlayback = useCallback(() => {\n const playback = playbackRef.current;\n if (!playback) return;\n playbackRef.current = null;\n playback.abort.abort();\n playback.audio?.pause();\n if (playback.url) URL.revokeObjectURL(playback.url);\n setSpeakingId((current) => (current === playback.id ? null : current));\n }, []);\n\n const controller = useCallback((): SpeechController | null => {\n if (!browserSupported) return null;\n controllerRef.current ??= createSpeechController<SpeechSynthesisUtterance>(\n window.speechSynthesis,\n (text, { lang, onEnd, onError }) => {\n const utterance = new SpeechSynthesisUtterance(text);\n if (lang) utterance.lang = lang;\n utterance.onend = onEnd;\n utterance.onerror = onError;\n return utterance;\n },\n setSpeakingId,\n );\n return controllerRef.current;\n }, [browserSupported]);\n\n /**\n * Reads the message `id` from the markdown documents the transcript renders it\n * from (`answerMarkdownSources`), or stops it when it is the one being read.\n */\n const speakInBrowser = useCallback((id: string, text: string) => controller()?.speak(id, chunkSpeech(text), localeRef.current), [controller]);\n\n const speakThroughRoute = useCallback(\n async (route: TtsRoute, id: string, text: string) => {\n const abort = new AbortController();\n const playback: { id: string; audio?: HTMLAudioElement; url?: string; abort: AbortController } = { id, abort };\n playbackRef.current = playback;\n setSpeakingId(id);\n // A long answer goes as several requests, read one after the other on one\n // audio element (made while the click is still being handled, which some\n // browsers ask of playback), the next one fetched while this one plays.\n const chunks = chunkSpeech(text, TTS_MAX_LENGTH);\n const audio = new Audio();\n playback.audio = audio;\n let index = 0;\n try {\n for await (const blob of speechAhead(panelFetch, route, chunks, abort.signal)) {\n if (playbackRef.current !== playback) return;\n if (!blob) throw new Error('TTS failed');\n if (playback.url) URL.revokeObjectURL(playback.url);\n playback.url = URL.createObjectURL(blob);\n const ended = new Promise<boolean>((resolve) => {\n audio.onended = () => resolve(true);\n audio.onerror = () => resolve(false);\n });\n audio.src = playback.url;\n await audio.play();\n if (!(await ended)) throw new Error('TTS playback failed');\n if (playbackRef.current !== playback) return;\n index += 1;\n }\n stopPlayback();\n } catch (err) {\n if (playbackRef.current !== playback || (err instanceof DOMException && err.name === 'AbortError')) return;\n // The route failed: the browser's voice reads what is left, when there is one.\n stopPlayback();\n speakInBrowser(id, chunks.slice(index).join(' '));\n }\n },\n [speakInBrowser, stopPlayback],\n );\n\n const toggle = useCallback(\n (id: string, documents: readonly string[]) => {\n const reading = playbackRef.current?.id ?? controllerRef.current?.speakingId ?? null;\n stopPlayback();\n controllerRef.current?.stop();\n if (reading === id) return;\n const text = speakableText(documents);\n const route = ttsRef.current;\n if (route) void speakThroughRoute(route, id, text);\n else speakInBrowser(id, text);\n },\n [speakInBrowser, speakThroughRoute, stopPlayback],\n );\n\n const stop = useCallback(() => {\n stopPlayback();\n controllerRef.current?.stop();\n }, [stopPlayback]);\n\n useEffect(() => () => stop(), [stop]);\n\n return { supported, speakingId, toggle, stop };\n}\n","/**\n * The time shown under a chat message.\n *\n * A message from today reads as its time alone (`14:05`); an older one leads\n * with a short date (`29 Sept, 14:05`), and one from another year carries the\n * year, so a restored conversation says when each answer was given without\n * anyone having to hover. The tooltip spells the whole date out.\n *\n * Kept free of local imports so `node --test` runs it as is.\n */\n\nexport interface MessageTime {\n /** What the footer shows. */\n label: string;\n /** The complete date and time, for the tooltip. */\n full: string;\n /** Machine-readable, for the `dateTime` attribute of a `<time>` element. */\n iso: string;\n}\n\ntype FormatKind = 'time' | 'date' | 'dateWithYear' | 'full';\n\nconst FORMAT_OPTIONS: Record<FormatKind, Intl.DateTimeFormatOptions> = {\n time: { hour: '2-digit', minute: '2-digit' },\n date: { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' },\n dateWithYear: { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' },\n full: { dateStyle: 'full', timeStyle: 'short' },\n};\n\n// Every row of a transcript formats with the same few formatters, and building\n// one costs far more than using it.\nconst formatters = new Map<string, Intl.DateTimeFormat>();\n\nfunction formatter(locale: string | undefined, kind: FormatKind): Intl.DateTimeFormat {\n const key = `${locale ?? ''}|${kind}`;\n let cached = formatters.get(key);\n if (!cached) {\n try {\n cached = new Intl.DateTimeFormat(locale || undefined, FORMAT_OPTIONS[kind]);\n } catch {\n // A malformed tag throws a RangeError: fall back to the browser's own\n // language rather than losing the time.\n cached = new Intl.DateTimeFormat(undefined, FORMAT_OPTIONS[kind]);\n }\n formatters.set(key, cached);\n }\n return cached;\n}\n\nfunction sameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\n/**\n * The footer time of a message sent at `date`, in `locale` (browser default\n * when absent), read against `now`. `null` for a date that is not one, so a\n * backend sending an unparseable `created_at` loses the time, not the row.\n */\nexport function formatMessageTime(date: Date, locale?: string, now: Date = new Date()): MessageTime | null {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;\n const kind: FormatKind = sameDay(date, now) ? 'time' : date.getFullYear() === now.getFullYear() ? 'date' : 'dateWithYear';\n return {\n label: formatter(locale, kind).format(date),\n full: formatter(locale, 'full').format(date),\n iso: date.toISOString(),\n };\n}\n","/**\n * How the thread keeps the end of the conversation in view as it grows.\n *\n * The view follows the conversation while the reader is at the bottom, stops\n * once they scroll up to read back, and follows again when they come back down\n * or send a message. While following, each streamed frame jumps to the end at\n * once: a smooth scroll restarted on every frame lags behind the text. A\n * message just sent and a conversation just opened are scrolled to smoothly,\n * and since any other scroll aborts a smooth one, the frames streamed in the\n * meantime wait for it to arrive and the view then catches up with them.\n *\n * The reader's gestures toward the top (a wheel, a swipe, a key, a press on\n * the scrollbar) stop the follow before the scroll they cause, read by the\n * predicates below. A scroll event that moved nothing changes nothing: it is\n * the event of a jump of ours, delivered a frame late, and right after such a\n * gesture it would re-arm the follow the gesture just ended. Each move is read\n * against the last position that moved more than 1 px, so a slow drift of\n * sub-pixel steps adds up to a move instead of reading as none at every step.\n */\n\n/** How near the bottom a reader scrolling down must come for the view to follow again. */\nexport const FOLLOW_THRESHOLD_PX = 100;\n\n/** How far (px) a finger must travel down the screen before the swipe counts as scrolling up. */\nexport const TOUCH_SLOP_PX = 4;\n\n/**\n * The longest a smooth scroll of ours is waited for. Browsers finish one well\n * within it; past it the view catches up anyway, since a smooth scroll cut\n * short by another one (a prompt scrolling itself into view) never reaches\n * where it was going.\n */\nconst SMOOTH_SCROLL_TIMEOUT_MS = 1000;\n\n/** The scroll container, as far as following the conversation needs it. */\nexport interface ScrollBox {\n readonly scrollTop: number;\n readonly scrollHeight: number;\n readonly clientHeight: number;\n scrollTo(options: ScrollToOptions): void;\n}\n\nexport interface ScrollFollowTimers {\n set: (callback: () => void, ms: number) => unknown;\n clear: (handle: unknown) => void;\n}\n\nexport interface ScrollFollower {\n /** Whether the view follows the conversation as it grows. */\n readonly following: boolean;\n /** Scroll smoothly to the end and follow again: a message just sent, a conversation just opened. */\n reveal: () => void;\n /** The conversation grew: keep its end in view while the view follows. */\n keepUp: () => void;\n /** The container scrolled. */\n onScroll: () => void;\n /**\n * The reader is scrolling toward the top (a wheel, a swipe, a key, the scrollbar). Stops the follow\n * at once: the scroll event only comes with the next frame, and a streamed\n * frame committed before it would pull the view back down first.\n */\n leave: () => void;\n}\n\nconst browserTimers: ScrollFollowTimers = {\n set: (callback, ms) => setTimeout(callback, ms),\n clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),\n};\n\nexport function createScrollFollower(getBox: () => ScrollBox | null, timers: ScrollFollowTimers = browserTimers): ScrollFollower {\n let following = true;\n // Where the view was last seen, which tells the reader scrolling up apart\n // from the view being moved down by a scroll of ours.\n let lastTop = 0;\n // A smooth scroll of ours still running: where it lands, and the timer that\n // stops waiting for it.\n let smooth: { target: number; timer: unknown } | null = null;\n\n // 'instant' rather than 'auto': a `scroll-behavior: smooth` stylesheet would\n // turn 'auto' smooth again.\n const jumpToEnd = (box: ScrollBox) => {\n box.scrollTo({ top: box.scrollHeight, behavior: 'instant' });\n lastTop = box.scrollTop;\n };\n\n const stopWaiting = () => {\n if (!smooth) return;\n timers.clear(smooth.timer);\n smooth = null;\n };\n\n // The smooth scroll is over: catch up with what streamed in while it ran.\n const arrived = () => {\n stopWaiting();\n const box = getBox();\n if (box && following) jumpToEnd(box);\n };\n\n return {\n get following() {\n return following;\n },\n reveal: () => {\n const box = getBox();\n if (!box) return;\n following = true;\n stopWaiting();\n const target = Math.max(0, box.scrollHeight - box.clientHeight);\n box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });\n lastTop = box.scrollTop;\n // Already at the end, or moved there at once (smooth scrolling turned off).\n if (lastTop >= target - 1) return;\n smooth = { target, timer: timers.set(arrived, SMOOTH_SCROLL_TIMEOUT_MS) };\n },\n keepUp: () => {\n const box = getBox();\n // A jump now would cut the smooth scroll short: it catches up on arrival.\n if (box && following && !smooth) jumpToEnd(box);\n },\n onScroll: () => {\n const box = getBox();\n if (!box) return;\n const top = box.scrollTop;\n const movedUp = top < lastTop - 1;\n const movedDown = top > lastTop + 1;\n if (movedUp || movedDown) lastTop = top;\n const distance = box.scrollHeight - top - box.clientHeight;\n if (movedUp) {\n // The reader scrolled up — or the browser moved the view to the new end\n // of a list that got shorter, which leaves it at the bottom.\n following = distance <= 1;\n } else if (movedDown && distance <= FOLLOW_THRESHOLD_PX) {\n following = true;\n }\n // Ours only goes down: it is over once the view reaches where it was sent\n // or the end, and a move up is the reader taking over.\n if (smooth && (movedUp || top >= smooth.target - 1 || distance <= 1)) arrived();\n },\n leave: () => {\n const box = getBox();\n // With nothing above to scroll to (a thread that fits), the reader is\n // still at the end whatever the gesture.\n if (!box || box.scrollTop <= 0) return;\n following = false;\n stopWaiting();\n },\n };\n}\n\n/** A wheel turned toward the top. A pinch (ctrl + wheel) zooms, and a mostly sideways one pans a code block or a table. */\nexport function wheelScrollsUp(event: { deltaX: number; deltaY: number; ctrlKey: boolean }): boolean {\n return !event.ctrlKey && event.deltaY < 0 && Math.abs(event.deltaY) > Math.abs(event.deltaX);\n}\n\nconst KEYS_TOWARD_TOP = new Set(['ArrowUp', 'PageUp', 'Home']);\n\n/** A key that scrolls a focused thread toward its top. */\nexport function keyScrollsUp(event: { key: string; shiftKey: boolean; altKey: boolean }): boolean {\n if (event.altKey) return false;\n return KEYS_TOWARD_TOP.has(event.key) || (event.key === ' ' && event.shiftKey);\n}\n\n/**\n * A finger moving DOWN the screen (the content follows it toward the top),\n * measured from the highest point of the touch so far, so a tap's jitter is\n * not a swipe.\n */\nexport function swipeScrollsUp(anchorY: number, y: number): boolean {\n return y - anchorY > TOUCH_SLOP_PX;\n}\n\n/**\n * Where the one finger on the screen is. `null` while two or more are down: a\n * pinch zooms and a first finger can travel down past the slop without the\n * thread scrolling at all.\n */\nexport function singleTouchY(touches: ArrayLike<{ clientY: number }>): number | null {\n return touches.length === 1 ? touches[0].clientY : null;\n}\n\n/**\n * The swipe anchor after a `touchmove` that found the finger at `y`: none\n * during a pinch, the finger's own position on its first move after one (no\n * `touchstart` marks the finger left on the screen), else the highest point.\n */\nexport function nextTouchAnchor(anchor: number | null, y: number | null): number | null {\n if (y === null) return null;\n return anchor === null ? y : Math.min(anchor, y);\n}\n\n/** A pointer press on the thread, `x` / `y` measured from its padding box. */\nexport interface ScrollPress {\n button: number;\n /** The press landed on the container itself, not on its content. */\n onContainer: boolean;\n /** The press landed on a link, a button or a field. */\n onControl: boolean;\n x: number;\n y: number;\n clientWidth: number;\n clientHeight: number;\n scrollTop: number;\n scrollHeight: number;\n}\n\n/**\n * A press that hands the view to the reader: the middle button away from a\n * control, which starts the browser's autoscroll on Windows and Linux (on a\n * link it opens a tab), or the thread's own scrollbar (`x` past its content\n * box) on the thumb or above it. The track below the thumb pages DOWN, toward\n * the end the view follows.\n */\nexport function pressTakesScrollbar(press: ScrollPress): boolean {\n if (press.button === 1) return !press.onControl;\n if (press.button !== 0 || !press.onContainer || press.x < press.clientWidth) return false;\n const thumbBottom = ((press.scrollTop + press.clientHeight) / press.scrollHeight) * press.clientHeight;\n return press.y <= thumbBottom;\n}\n\n/** What `innerBoxScrollsUp` reads of an element. */\nexport interface ScrollNode {\n readonly scrollTop: number;\n readonly scrollHeight: number;\n readonly clientHeight: number;\n}\n\nconst SCROLLING_OVERFLOW = new Set(['auto', 'scroll', 'overlay']);\n\n/**\n * Whether a gesture toward the top at `target` scrolls a box INSIDE the thread\n * instead of the thread: the browser moves the innermost box under it that can\n * still go up, and the thread only once that box is at its top. A box that\n * only clips (`overflow: hidden` — the reasoning window, pinned to its own\n * end) is not one: the gesture goes through it.\n */\nexport function innerBoxScrollsUp<T extends ScrollNode>(\n target: T | null,\n container: T,\n parentOf: (node: T) => T | null,\n overflowY: (node: T) => string,\n): boolean {\n for (let node = target; node && node !== container; node = parentOf(node)) {\n if (node.scrollTop > 0 && node.scrollHeight > node.clientHeight && SCROLLING_OVERFLOW.has(overflowY(node))) {\n return true;\n }\n }\n return false;\n}\n","import { useEffect, useState } from 'react';\nimport type { ChatArtifact, ChatArtifactKind, Translate } from '../types';\nimport { artifactDownloadLabel, artifactFacts, artifactKindLabel, artifactLink, isRenderInProgress, translate, videoRenderLabel } from '../utils';\nimport { pollInTurn } from '../utils/polling';\nimport { ClapperboardIcon, DownloadIcon, ExternalLinkIcon, FileTextIcon, PresentationIcon } from './icons';\nimport { Spinner } from './Spinner';\n\n/** How often a card asks again about a video that is still rendering, and for how long. */\nconst REFRESH_INTERVAL_MS = 10_000;\nconst REFRESH_FOR_MS = 15 * 60_000;\n\nconst KIND_ICONS: Record<ChatArtifactKind, typeof PresentationIcon> = {\n presentation: PresentationIcon,\n report: FileTextIcon,\n video: ClapperboardIcon,\n};\n\nconst ACTION =\n 'inline-flex items-center gap-1 rounded-md border border-gray-200 dark:border-white/10 px-2 py-1 text-[0.7rem] text-gray-700 dark:text-white/70 transition-colors hover:border-[var(--chat-accent)] hover:text-[var(--chat-accent)] disabled:opacity-50 disabled:cursor-default outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)]';\n\ninterface ArtifactCardProps {\n artifact: ChatArtifact;\n t: Translate;\n /** Downloads it in a format through the host's proxy; no download buttons without it. */\n onDownload?: (artifact: ChatArtifact, format: string) => Promise<void>;\n /** Describes it again; a video still rendering is followed with it. */\n onRefresh?: (kind: ChatArtifactKind, id: string) => Promise<ChatArtifact | undefined>;\n /** Opens it in the host itself, in place of the link to XTM One. */\n onOpen?: (artifact: ChatArtifact) => void;\n}\n\n/**\n * A deck, report or video an answer made: what it is, `Open in XTM One`, and\n * its downloads when the host proxies them. The panel cannot draw the deck or\n * play the film itself; XTM One's page does.\n */\nexport const ArtifactCard = ({ artifact, t, onDownload, onRefresh, onOpen }: ArtifactCardProps) => {\n const [current, setCurrent] = useState(artifact);\n const [busyFormat, setBusyFormat] = useState<string | null>(null);\n const [gaveUp, setGaveUp] = useState(false);\n useEffect(() => setCurrent(artifact), [artifact]);\n\n const following = !!onRefresh && !gaveUp && isRenderInProgress(current);\n useEffect(() => {\n if (!following || !onRefresh) return undefined;\n let cancelled = false;\n const stop = pollInTurn(\n () =>\n onRefresh(current.kind, current.id).then((fresh) => {\n if (!cancelled && fresh) setCurrent(fresh);\n }),\n { intervalMs: REFRESH_INTERVAL_MS, forMs: REFRESH_FOR_MS, onGiveUp: () => setGaveUp(true) },\n );\n return () => {\n cancelled = true;\n stop();\n };\n }, [following, onRefresh, current.kind, current.id]);\n\n const Icon = KIND_ICONS[current.kind];\n const kindLabel = artifactKindLabel(current.kind, t);\n const title = current.title || kindLabel;\n const facts = artifactFacts(current, t);\n const renderLabel = videoRenderLabel(current, t);\n const link = onOpen ? null : artifactLink(current);\n const downloads = onDownload ? current.downloads : [];\n\n const download = async (format: string) => {\n if (!onDownload) return;\n setBusyFormat(format);\n try {\n await onDownload(current, format);\n } finally {\n setBusyFormat(null);\n }\n };\n\n return (\n <div\n role=\"group\"\n aria-label={`${kindLabel}: ${title}`}\n className=\"flex flex-col gap-2 w-full max-w-[90%] rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/[0.04] px-3 py-2.5\"\n >\n <div className=\"flex items-start gap-2.5 min-w-0\">\n <span className=\"shrink-0 mt-0.5 text-[var(--chat-accent)]\">\n <Icon size={18} />\n </span>\n <span className=\"flex flex-col min-w-0 flex-1 gap-0.5\">\n <span className=\"truncate text-[0.8125rem] font-medium text-gray-900 dark:text-white\">{title}</span>\n {facts && <span className=\"text-[0.7rem] text-gray-500 dark:text-white/50\">{facts}</span>}\n {renderLabel && (\n <span className=\"flex items-center gap-1.5 text-[0.7rem] text-gray-500 dark:text-white/50\" aria-live=\"polite\">\n {following && <Spinner size={10} />}\n {renderLabel}\n </span>\n )}\n </span>\n </div>\n {(link || onOpen || downloads.length > 0) && (\n <div className=\"flex flex-wrap items-center gap-1.5 pl-[28px]\">\n {onOpen && (\n <button type=\"button\" className={ACTION} onClick={() => onOpen(current)}>\n {t('Open')}\n </button>\n )}\n {link && (\n <a href={link} target=\"_blank\" rel=\"noopener noreferrer\" className={ACTION}>\n {t('Open in XTM One')}\n <ExternalLinkIcon size={12} />\n </a>\n )}\n {downloads.map((format) => {\n const label = artifactDownloadLabel(format, t);\n return (\n <button\n key={format}\n type=\"button\"\n className={ACTION}\n disabled={busyFormat !== null}\n aria-label={translate(t, 'Download {format}', { format: label })}\n onClick={() => void download(format)}\n >\n {busyFormat === format ? <Spinner size={11} /> : <DownloadIcon size={12} />}\n {label}\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n","import type { IconProps } from '../../types';\n\nexport const PresentationIcon = ({ 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=\"M2 3h20\" />\n <path d=\"M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3\" />\n <path d=\"m7 21 5-5 5 5\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const FileTextIcon = ({ 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 <path d=\"M10 9H8\" />\n <path d=\"M16 13H8\" />\n <path d=\"M16 17H8\" />\n </svg>\n);\n","import type { IconProps } from '../../types';\n\nexport const ClapperboardIcon = ({ 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.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3Z\" />\n <path d=\"m6.2 5.3 3.1 3.9\" />\n <path d=\"m12.4 3.4 3.1 4\" />\n <path d=\"M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z\" />\n </svg>\n);\n","/**\n * Calls *ask* every *intervalMs*, one call at a time: the next one waits for\n * the last to settle, so a slow answer can never land after a newer one. A\n * call that fails is followed by the next as usual. After *forMs* it stops for\n * good and calls *onGiveUp*, even with a call still unanswered; the returned\n * function stops it earlier.\n */\nexport function pollInTurn(\n ask: () => Promise<unknown>,\n { intervalMs, forMs, onGiveUp }: { intervalMs: number; forMs: number; onGiveUp?: () => void },\n): () => void {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const next = () => {\n timer = setTimeout(() => {\n ask()\n .catch(() => undefined)\n .finally(() => {\n if (!stopped) next();\n });\n }, intervalMs);\n };\n const deadline = setTimeout(() => {\n stopped = true;\n clearTimeout(timer);\n onGiveUp?.();\n }, forMs);\n next();\n return () => {\n stopped = true;\n clearTimeout(timer);\n clearTimeout(deadline);\n };\n}\n","import { useEffect, useId, useRef, useState } from 'react';\nimport type { ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict } from '../types';\nimport { translate } from '../utils';\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 data-testid=\"filigran-chat-approval\" 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 {/* An unnamed tool is a malformed proposal, not a tool called\n \"unknown tool\": the placeholder is the package's own text and\n therefore translated here, not in the parser. */}\n <p className=\"truncate font-mono text-xs text-gray-800 dark:text-white/85\">{proposal.toolName || t('unknown tool')}</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 ? translate(t, '{decided}/{total} decided', { decided: decidedCount, total: proposals.length }) : ''}\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';\nimport { usePanelFetch } from '../hooks/usePanelRequests';\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 const panelFetch = usePanelFetch();\n\n useEffect(() => {\n if (!needsAuth) return;\n let cancelled = false;\n let objectUrl: string | null = null;\n\n panelFetch(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, useState, type ReactNode } from 'react';\nimport type { Translate } from '../types';\nimport { ExternalLinkIcon, GamepadIcon } from './icons';\n\n/**\n * Playful, rotating \"still working on it\" messages shown below the status\n * bubble during longer waits. Kept short and upbeat so they fit on one line in\n * the narrow floating panel and read as a sense of progress rather than noise.\n */\nconst defaultMessages = (t: Translate) => [\n t('Crunching the data'),\n t('Connecting the dots'),\n t('Consulting the sources'),\n t('Thinking it through'),\n t('Reticulating splines'),\n t('Analyzing the details'),\n t('Almost there'),\n t('Putting it together'),\n t('Polishing the answer'),\n t('Wrapping things up'),\n];\n\n/** Message rotation cadence. */\nconst ROTATE_MS = 2600;\n\n/**\n * Where the waiting game is played: the host's own *gameUrl* when it names\n * one, else XTM One's arcade, which `?arcade=play` opens on any XTM One page,\n * the chat being where it lives. Both are host input that ends up in an href,\n * so only an http(s) URL is linked, or (*gameUrl*) a path of the host's origin.\n */\nexport function waitingGameUrl(xtmOneUrl: string | undefined, gameUrl?: string): string | null {\n if (gameUrl !== undefined) {\n const url = gameUrl.trim();\n return /^https?:\\/\\//i.test(url) || /^\\/(?![/\\\\])/.test(url) ? url : null;\n }\n const base = xtmOneUrl?.trim().replace(/\\/+$/, '');\n if (!base || !/^https?:\\/\\//i.test(base)) return null;\n return `${base}/chat?arcade=play`;\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\ninterface ChatWaitingPanelProps {\n t: Translate;\n /** Host-level override; when false the panel is hidden entirely. */\n enabled?: boolean;\n /** Opens the arcade in place, for a host that runs it itself. Wins over `playUrl`. */\n onPlay?: () => void;\n /** Where the arcade is played, opened in a new tab. */\n playUrl?: string | null;\n /** The thread's follower: keeps its end in view while the reader is there. */\n keepEndInView: () => void;\n /** The host's rotating messages, already translated; the package's otherwise. */\n messages?: string[];\n /** The host's own waiting content, in place of the messages and the invitation to play. */\n content?: ReactNode;\n}\n\nconst PLAY_CLASS =\n 'inline-flex items-center gap-1.5 self-start rounded text-xs text-[var(--chat-accent)] opacity-80 transition-opacity hover:opacity-100 focus-visible:opacity-100';\n\n/**\n * Waiting experience shown below the status bubble during longer waits:\n * rotating messages, and an invitation to play XTM One's Space Invaders arcade\n * while the agent works. The package ships no game of its own; without a way\n * to reach the arcade the messages stand alone.\n */\nexport const ChatWaitingPanel = ({ t, enabled = true, onPlay, playUrl, keepEndInView, messages: hostMessages, content }: ChatWaitingPanelProps) => {\n const messages = useMemo(() => (hostMessages && hostMessages.length > 0 ? hostMessages : defaultMessages(t)), [hostMessages, t]);\n const reducedMotion = usePrefersReducedMotion();\n const [msgIndex, setMsgIndex] = useState(0);\n\n // The panel mounts below the last message but, unlike streamed reasoning or\n // answer text (which auto-scrolls on length change), nothing else triggers a\n // scroll - so at the bottom it lands under the fold. It is revealed through\n // the thread's follower, which only moves a reader still following the end:\n // one who scrolled up stays where they went, however near the end.\n useEffect(() => {\n if (enabled) keepEndInView();\n }, [enabled, keepEndInView]);\n\n // A disabled host arms no timer; `enabled` is a dep so toggling it cleans up.\n useEffect(() => {\n if (!enabled || content) return;\n const id = window.setInterval(() => setMsgIndex((i) => (i + 1) % messages.length), ROTATE_MS);\n return () => window.clearInterval(id);\n }, [enabled, messages.length, !!content]);\n\n if (!enabled) return null;\n if (content) return <div className=\"ml-11 mt-2.5 max-w-[78%]\">{content}</div>;\n\n const current = messages[msgIndex % messages.length];\n\n return (\n <div className=\"ml-11 mt-2.5 max-w-[78%]\">\n {/* Keep the live region scoped to the announced text only: wrapping the\n play link too would re-announce it alongside each message change. */}\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {current}\n </span>\n {/* Every non-essential animation (fade-ins, the blinking caret) stops\n under prefers-reduced-motion; only the dimmed text rotates. */}\n <div\n className=\"flex flex-col gap-1.5 rounded-md bg-[var(--chat-accent)]/[0.03] px-3 py-2.5\"\n style={reducedMotion ? undefined : { animation: 'chat-fade-in 0.5s ease-out' }}\n >\n <span\n key={current}\n aria-hidden\n className=\"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 {onPlay ? (\n <button type=\"button\" onClick={onPlay} className={PLAY_CLASS}>\n <span aria-hidden className=\"inline-flex\">\n <GamepadIcon size={13} />\n </span>\n {t('Play Space Invaders')}\n </button>\n ) : playUrl ? (\n <a href={playUrl} target=\"_blank\" rel=\"noopener noreferrer\" className={PLAY_CLASS}>\n <span aria-hidden className=\"inline-flex\">\n <GamepadIcon size={13} />\n </span>\n {t('Play Space Invaders in XTM One')}\n <span aria-hidden className=\"inline-flex\">\n <ExternalLinkIcon size={11} />\n </span>\n </a>\n ) : null}\n </div>\n </div>\n );\n};\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport type { AgentStatusState, IconProps, Translate } from '../types';\n\n/** What a host can change of the status line and the waiting panel. */\nexport interface ThinkingOverrides {\n /** The status line's text, over the built-in one; `undefined` keeps it. */\n statusLabel?: (status: AgentStatusState) => string | undefined;\n /** The waiting panel's rotating messages, already translated. */\n waitingMessages?: string[];\n /** Replaces the waiting panel's content. */\n renderWaitingPanel?: () => React.ReactNode;\n}\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 { translate } from '../utils';\nimport { ChatWaitingPanel } from './ChatWaitingPanel';\n\ninterface ChatThinkingProps extends ThinkingOverrides {\n agentStatus: AgentStatusState | null;\n logoIcon?: React.ReactNode;\n t: Translate;\n /** Host-level override for the waiting panel (messages and the invitation to play). */\n miniGameEnabled?: boolean;\n /** Opens the XTM One arcade in place; wins over `waitingGameUrl`. */\n onPlayWaitingGame?: () => void;\n /** Where the XTM One arcade is played, opened in a new tab. */\n waitingGameUrl?: string | null;\n /** The thread's follower: keeps its end in view while the reader is there. */\n keepEndInView: () => void;\n /** Ends an agent's announced wait early; offered while it waits. */\n onSkipWait?: () => void;\n}\n\ntype IconComponent = (props: IconProps) => React.JSX.Element;\n\ninterface StatusVisual {\n label: string;\n StatusIcon: IconComponent;\n showDots: boolean;\n}\n\n/**\n * Whole-sentence keys, one per plural branch: a lookup-only `t` cannot select a\n * plural form, so the branch is chosen here and the locale translates the\n * finished sentence — including where the count sits in it.\n */\nfunction delegatingLabel(count: number, t: Translate): string {\n return count > 1 ? translate(t, 'Delegating {count} tasks…', { count }) : t('Delegating one task…');\n}\n\nfunction pollingLabel(count: number, t: Translate): string {\n return count > 1 ? translate(t, 'Waiting for {count} background tasks…', { count }) : t('Waiting for one background task…');\n}\n\nfunction collectingLabel(count: number, t: Translate): string {\n return count > 1 ? translate(t, 'Collecting results from {count} tasks…', { count }) : t('Collecting results from one task…');\n}\n\nfunction resolveStatusVisual(agentStatus: AgentStatusState | null, t: Translate): 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 return { label: delegatingLabel(count, t), 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 return { label: pollingLabel(count, t), 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 return { label: collectingLabel(count, t), 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]}…` : translate(t, '{tool} (+{count} more)…', { tool: unique[0], count: unique.length - 1 });\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 'cancelling':\n return { label: t('Stopping…'), StatusIcon: BrainIcon, showDots: true };\n case 'wait_start':\n return { label: agentStatus.waitReason || t('Waiting before continuing…'), StatusIcon: SparklesIcon, showDots: false };\n case 'acting_on_page':\n return { label: t('Acting on the page…'), StatusIcon: SparklesIcon, showDots: true };\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] ?? t('the agent');\n return { label: translate(t, 'Consulting {agent}…', { agent: consultName }), StatusIcon: UserPlusIcon, showDots: false };\n }\n case 'delegating': {\n const count = agentStatus.tools?.filter((n) => n === 'spawn_background_task').length ?? 0;\n return { label: delegatingLabel(count, t), StatusIcon: UserPlusIcon, showDots: false };\n }\n case 'polling': {\n const checkCount = agentStatus.tools?.filter((n) => n === 'check_task_status').length ?? 0;\n return { label: pollingLabel(checkCount, t), StatusIcon: SparklesIcon, showDots: false };\n }\n case 'collecting': {\n const fetchCount = agentStatus.tools?.filter((n) => n === 'get_task_result').length ?? 0;\n return { label: collectingLabel(fetchCount, t), StatusIcon: SparklesIcon, showDots: false };\n }\n case 'transferring': {\n const targetName = agentStatus.tools?.[0] ?? t('the agent');\n return { label: translate(t, 'Transferring to {agent}…', { agent: targetName }), StatusIcon: ExternalLinkIcon, showDots: false };\n }\n case 'thinking':\n default:\n return { label: t('Thinking...'), StatusIcon: BrainIcon, showDots: false };\n }\n}\n\n/** The status line's text for *agentStatus*: the host's (`statusLabel`), else the built-in one. */\nexport function statusLineText(agentStatus: AgentStatusState | null, t: Translate, statusLabel?: ThinkingOverrides['statusLabel']): string {\n return (agentStatus && statusLabel?.(agentStatus)) || resolveStatusVisual(agentStatus, t).label;\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 * Most characters of the turn's reasoning the window renders. The window is\n * pinned to its newest line and shows about eight lines, while a long turn\n * accumulates hundreds of KB: laying all of it out again on every streamed\n * chunk only re-flows text nobody can see.\n */\nconst REASONING_WINDOW_MAX_CHARS = 6000;\n\n/** Fewer visible characters than this is no reasoning to show. */\nconst MIN_REASONING_CHARS = 3;\n\n/**\n * The text the reasoning window shows: the cleaned tail of the prose between\n * the turn's fenced blocks, opened at a paragraph break when one is near.\n * Fences pair up from the start of the whole text, as `cleanReasoningText`\n * pairs them: paired inside a cut tail instead, a block the cut split or a\n * block still streaming shifts every pair and strips the newest prose.\n */\nfunction reasoningWindowText(content: string): string {\n if (content.length <= REASONING_WINDOW_MAX_CHARS) return cleanReasoningText(content);\n // [start, end) of the prose around each fenced block; an unclosed fence\n // stays prose, as `cleanReasoningText` leaves it.\n const prose: [number, number][] = [];\n let from = 0;\n for (let open = content.indexOf('```'); open !== -1; open = content.indexOf('```', from)) {\n const close = content.indexOf('```', open + 3);\n if (close === -1) break;\n prose.push([from, open]);\n from = close + 3;\n }\n prose.push([from, content.length]);\n const parts: string[] = [];\n let budget = REASONING_WINDOW_MAX_CHARS;\n for (let i = prose.length - 1; i >= 0 && budget > 0; i--) {\n const [start, end] = prose[i];\n const cut = Math.max(start, end - budget);\n parts.push(content.slice(cut, end));\n // + 1: each dropped block leaves a space, as in `cleanReasoningText`.\n budget -= end - cut + 1;\n }\n let tail = parts.reverse().join(' ');\n if (budget <= 0) {\n const paragraph = tail.indexOf('\\n\\n');\n if (paragraph !== -1 && paragraph < tail.length / 2) tail = tail.slice(paragraph + 2);\n }\n return cleanReasoningText(tail);\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. `text` comes cleaned and tail-bounded from\n * `reasoningWindowText`; the glow lives on `.filigran-chat-reasoning` so\n * reduced motion can stop it.\n */\nfunction ThinkingTextBubble({ text }: { text: string }) {\n const ref = useRef<HTMLDivElement>(null);\n const [isOverflowing, setIsOverflowing] = useState(false);\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 }, [text]);\n\n return (\n <div className=\"filigran-chat-reasoning 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 <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\">{text}</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 * How long a working turn with no reasoning to show waits before the waiting\n * panel fills the space, so a quick reply never flashes it.\n */\nconst WAITING_GAME_DELAY_MS = 5000;\n\n/**\n * How long reasoning already on screen must go without a new chunk before the\n * waiting panel takes its place: long enough to outlast an ordinary tool call,\n * so the window only gives way on a genuinely long wait.\n */\nconst QUIET_REASONING_GAME_DELAY_MS = 10_000;\n\n/**\n * True once `active` has held for `delayMs` with the same `key`; false the\n * moment either changes, in that very render (the timer that fired carries\n * the key it saw). Arms no timer while `active` is false, so a host that\n * disables the waiting panel schedules no timeouts or re-renders for it.\n */\nfunction useSustained(active: boolean, delayMs: number, key = 0): boolean {\n const [firedKey, setFiredKey] = useState<number | null>(null);\n useEffect(() => {\n setFiredKey(null);\n if (!active) return;\n const id = window.setTimeout(() => setFiredKey(key), delayMs);\n return () => window.clearTimeout(id);\n }, [active, delayMs, key]);\n return active && firedKey === key;\n}\n\nexport const ChatThinking = ({\n agentStatus,\n logoIcon,\n t,\n miniGameEnabled = true,\n onPlayWaitingGame,\n waitingGameUrl,\n keepEndInView,\n onSkipWait,\n statusLabel,\n waitingMessages,\n renderWaitingPanel,\n}: ChatThinkingProps) => {\n const visual = resolveStatusVisual(agentStatus, t);\n const { StatusIcon, showDots } = visual;\n const label = statusLineText(agentStatus, t, statusLabel);\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 waitStartMs = agentStatus?.status === 'wait_start' ? agentStatus.waitStartMs : undefined;\n const tickAnchor = elapsedStartMs ?? waitStartMs;\n const [nowMs, setNowMs] = useState(() => Date.now());\n useEffect(() => {\n if (tickAnchor == null) return;\n setNowMs(Date.now());\n const id = window.setInterval(() => setNowMs(Date.now()), 1000);\n return () => window.clearInterval(id);\n }, [tickAnchor]);\n // An announced wait counts down to the moment the agent resumes.\n const waitSeconds = agentStatus?.waitSeconds;\n const waitLeftS = waitStartMs != null && waitSeconds != null ? Math.max(0, waitSeconds - (nowMs - waitStartMs) / 1000) : undefined;\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 // Reasoning wins over the waiting panel: once the turn has reasoning to show,\n // the window stays through tool calls and short silences until the answer\n // streams. The panel fills a wait with no reasoning after 5 s, and takes over\n // from reasoning that has gone 10 s without a new chunk, until the next one.\n const reasoningText = useMemo(() => reasoningWindowText(thinkingContent ?? ''), [thinkingContent]);\n const showReasoning = reasoningText.length >= MIN_REASONING_CHARS;\n const waitedWithoutReasoning = useSustained(miniGameEnabled && !showReasoning, WAITING_GAME_DELAY_MS);\n const reasoningWentQuiet = useSustained(miniGameEnabled && showReasoning, QUIET_REASONING_GAME_DELAY_MS, thinkingContent?.length ?? 0);\n const showWaiting = showReasoning ? reasoningWentQuiet : waitedWithoutReasoning;\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=\"filigran-chat-active-border 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 {waitLeftS !== undefined ? (\n <span className=\"text-xs text-gray-400 dark:text-white/30 tabular-nums shrink-0\">{formatElapsed(Math.ceil(waitLeftS))}</span>\n ) : (\n showElapsed && <span className=\"text-xs text-gray-400 dark:text-white/30 tabular-nums shrink-0\">{formatElapsed(elapsedS)}</span>\n )}\n {agentStatus?.status === 'wait_start' && onSkipWait && (\n <button\n type=\"button\"\n onClick={onSkipWait}\n className=\"ml-1 shrink-0 rounded-md border border-[var(--chat-accent)]/40 px-2 py-0.5 text-xs text-[var(--chat-accent)] hover:bg-[var(--chat-accent)]/10 transition-colors\"\n >\n {t('Continue now')}\n </button>\n )}\n </div>\n </div>\n </div>\n {showWaiting ? (\n <ChatWaitingPanel\n t={t}\n enabled={miniGameEnabled}\n onPlay={onPlayWaitingGame}\n playUrl={waitingGameUrl}\n keepEndInView={keepEndInView}\n messages={waitingMessages}\n content={renderWaitingPanel?.()}\n />\n ) : showReasoning ? (\n <ThinkingTextBubble text={reasoningText} />\n ) : null}\n </>\n );\n};\n","import { Component, type ErrorInfo, type ReactNode } from 'react';\n\ninterface HostRenderBoundaryProps {\n fallback: ReactNode;\n children: ReactNode;\n /** A new key (the content completed by the stream) gives the host's rendering another try. */\n resetKey: string;\n /** What failed, for the console: \"A chat marker card\", \"A chat code block renderer\". */\n label: string;\n}\n\n/**\n * What a host renders inside an answer (a marker card, a code block) that\n * throws takes only itself down: the answer around it, and the rest of the\n * thread, stay on screen.\n */\nexport class HostRenderBoundary extends Component<HostRenderBoundaryProps, { failed: boolean; resetKey: string }> {\n state = { failed: false, resetKey: this.props.resetKey };\n static getDerivedStateFromError() {\n return { failed: true };\n }\n static getDerivedStateFromProps(props: { resetKey: string }, state: { failed: boolean; resetKey: string }) {\n return props.resetKey === state.resetKey ? null : { failed: false, resetKey: props.resetKey };\n }\n componentDidCatch(error: unknown, info: ErrorInfo) {\n console.error(`${this.props.label} failed to render`, error, info.componentStack);\n }\n render() {\n return this.state.failed ? this.props.fallback : this.props.children;\n }\n}\n\n/** The host's rendering, called inside the boundary: a renderer that throws itself is caught too. */\nexport function HostRendered({ draw }: { draw: () => ReactNode }) {\n return <>{draw()}</>;\n}\n\n/** What shows in place of a host's rendering that failed. */\nexport function HostRenderFallback({ t }: { t: (key: string) => string }) {\n return <p className=\"pl-1 text-[0.75rem] italic text-gray-400 dark:text-white/40\">{t('This item could not be loaded.')}</p>;\n}\n","import remarkGfm from 'remark-gfm';\nimport remarkParse from 'remark-parse';\nimport { unified } from 'unified';\n\n/**\n * The top-level blocks of a streamed markdown answer.\n *\n * The live answer is re-rendered every time the stream delivers text, and\n * parsing it whole costs ~14 ms at 8K characters and ~75 ms at 40K, on the\n * host application's main thread: past a few thousand characters its frames\n * go to re-parsing text that is already on screen. Rendered block by block\n * with a memoized renderer per block, only the blocks that can still change\n * are parsed again.\n *\n * The boundaries come from the parser the panel renders with (remark-parse +\n * remark-gfm; remark-breaks only rewrites line breaks inside a block), so\n * rendering the blocks one after the other produces what one parse of the\n * whole text produces. A block is final once a later top-level block starts\n * after a blank line with a complete first line: a blank line closes a\n * paragraph, and a written line after one cannot rejoin a list, a quote or an\n * indented block, whatever is appended to it. A block that starts without a\n * blank line before it stays open with its predecessor (`#` alone is a heading\n * that interrupts the paragraph above; `#x` is that paragraph's next line).\n * Link reference definitions and footnote definitions resolve across the whole\n * document, from a quote or a list item too, so a text holding one renders as\n * one block. They are read off the tree the split parses anyway rather than\n * matched by a line pattern: a pattern misses a label spanning lines, and\n * keeps whole a text that only looks like a definition (a TypeScript index\n * signature in a code block, a regex's `[^a-z]`).\n *\n * XTM One's web chat splits its streaming answer by the same rules; a fix to\n * one belongs in the other.\n */\n\nconst blockParser = unified().use(remarkParse).use(remarkGfm).freeze();\n\ntype BlockNode = ReturnType<typeof blockParser.parse>['children'][number];\n\nexport interface MarkdownBlocks {\n /** The text the blocks cover; `blocks.join('') === source`. */\n source: string;\n /** What to render: the top-level blocks, or the whole text when it holds a definition. */\n blocks: string[];\n /** Offset of each top-level block in `source`, kept when the text renders whole. */\n starts: number[];\n /** Top-level blocks `[0, settled)` are final while text is appended to `source`. */\n settled: number;\n /** The first top-level block holding a link reference or footnote definition, `-1` for none. */\n firstDefinition: number;\n}\n\n/** Whether *node* is or contains a link reference or footnote definition. */\nfunction holdsDefinition(node: BlockNode): boolean {\n switch (node.type) {\n case 'definition':\n case 'footnoteDefinition':\n return true;\n case 'blockquote':\n case 'list':\n case 'listItem':\n return node.children.some(holdsDefinition);\n default:\n return false;\n }\n}\n\nfunction lineStart(text: string, offset: number): number {\n return text.lastIndexOf('\\n', offset - 1) + 1;\n}\n\n/**\n * Whether the blocks before the one at *start* are final: it starts after a\n * blank line, and its first line is complete - while that line is still being\n * written it can turn into a continuation of the block above (`1` is a\n * paragraph, `10.` the next item of the ordered list before it).\n */\nfunction settlesBefore(text: string, start: number): boolean {\n if (start < 2 || text.indexOf('\\n', start) === -1) return false;\n const previousLine = text.slice(lineStart(text, start - 1), start - 1);\n return previousLine.trim() === '';\n}\n\n/**\n * Whether the region ends on an indented line (4 spaces or a tab). micromark\n * keeps an indented code block open across the blank lines after it and parses\n * the next line as if it interrupted it - `2. two` is text there and an ordered\n * list on its own - so the next block is rendered with it.\n */\nfunction endsIndented(text: string, start: number, end: number): boolean {\n const lines = text.slice(start, end).split('\\n');\n for (let index = lines.length - 1; index >= 0; index--) {\n if (lines[index].trim()) return /^(?: {4}|\\t)/.test(lines[index]);\n }\n return false;\n}\n\nfunction single(text: string): MarkdownBlocks {\n return { source: text, blocks: text ? [text] : [], starts: text ? [0] : [], settled: 0, firstDefinition: -1 };\n}\n\n/**\n * Split *text* into top-level markdown blocks.\n *\n * With *previous* - the split of a text that *text* extends - the settled\n * blocks are reused and only the rest is parsed again. Any other *previous* (a\n * retracted answer, a normalizer that rewrote earlier lines) is ignored.\n */\nexport function splitMarkdownBlocks(text: string, previous?: MarkdownBlocks | null): MarkdownBlocks {\n if (!text.trim()) return single(text);\n\n const reuse = previous && previous.settled > 0 && text.startsWith(previous.source) ? previous.settled : 0;\n const from = reuse ? previous!.starts[reuse] : 0;\n const tail = text.slice(from);\n\n const starts = previous && reuse ? previous.starts.slice(0, reuse) : [];\n // A definition in a settled block stays one; none there means the first one,\n // if any, is in the part parsed now.\n let firstDefinition = previous && reuse && previous.firstDefinition < reuse ? previous.firstDefinition : -1;\n starts.push(from);\n for (const node of blockParser.parse(tail).children) {\n const offset = node.position?.start.offset;\n if (offset !== undefined) {\n const start = from + lineStart(tail, offset);\n const last = starts[starts.length - 1];\n if (start > last && !endsIndented(text, last, start)) starts.push(start);\n }\n if (firstDefinition < 0 && holdsDefinition(node)) firstDefinition = starts.length - 1;\n }\n\n let settled = reuse;\n for (let index = starts.length - 1; index > reuse; index--) {\n if (settlesBefore(text, starts[index])) {\n settled = index;\n break;\n }\n }\n\n const blocks = firstDefinition < 0 ? starts.map((start, index) => text.slice(start, starts[index + 1] ?? text.length)) : [text];\n return { source: text, blocks, starts, settled, firstDefinition };\n}\n\n/**\n * {@link splitMarkdownBlocks} with the previous split kept between calls, so a\n * text that grows by a few characters per call is re-parsed from its last open\n * block only. Correctness never depends on the kept split: a text that does not\n * extend it is split from the start.\n */\nexport class MarkdownBlockSplitter {\n private last: MarkdownBlocks | null = null;\n\n split(text: string): string[] {\n if (this.last?.source !== text) this.last = splitMarkdownBlocks(text, this.last);\n return this.last.blocks;\n }\n}\n","import { createContext, memo, useCallback, useContext, useMemo, useState } from 'react';\nimport Markdown, { type Components } from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport remarkBreaks from 'remark-breaks';\nimport { CheckIcon, CopyIcon } from './icons';\nimport { ChatImage } from './ChatImage';\nimport { HostRenderBoundary, HostRenderFallback } from './HostRenderBoundary';\nimport type { ChatCodeBlockRenderer } from '../types';\nimport { identity, markdownUrlTransform, prepareMarkdown } from '../utils';\nimport { MarkdownBlockSplitter } from '../utils/markdownBlocks';\nimport { isRelativeHref, toInternalHref } from '../utils/pageActions';\n\ninterface MarkdownMessageProps {\n content: string;\n /**\n * True while the message is still streaming: it is then rendered one\n * top-level block at a time, so a new chunk only re-parses the blocks it can\n * still change, and stays so once the stream ends. The markup is the one a\n * single parse of the finished message renders.\n */\n streaming?: boolean;\n onRelativeLinkClick?: (href: string) => void;\n /** Auth headers used to fetch host-relative images (see `ChatImage`). */\n requestHeaders?: Record<string, string>;\n /** The host's renderers for fenced code blocks, by lower-cased language. */\n codeBlockRenderers?: Record<string, ChatCodeBlockRenderer>;\n t?: (key: string) => string;\n}\n\n/**\n * What the element renderers need from the message they render.\n *\n * react-markdown uses each renderer as an element TYPE, so a renderer must keep\n * its identity across renders: a new function is a new type, and React remounts\n * everything rendered with it. The renderers are therefore module-level and\n * read what varies per message from here — with renderers recreated on every\n * render, the streaming answer's DOM was rebuilt on every chunk, a selection in\n * it was lost, and each image in it was fetched again.\n */\ninterface MarkdownRenderContextValue {\n copiedBlock: string | null;\n onCopyCode: (code: string) => void;\n onRelativeLinkClick?: (href: string) => void;\n requestHeaders?: Record<string, string>;\n codeBlockRenderers?: Record<string, ChatCodeBlockRenderer>;\n streaming: boolean;\n t: (key: string) => string;\n}\n\nconst MarkdownRenderContext = createContext<MarkdownRenderContextValue>({\n copiedBlock: null,\n onCopyCode: () => undefined,\n streaming: false,\n t: identity,\n});\n\n/** A host's code block, called inside its boundary; nothing drawn keeps the block as code. */\nfunction HostCodeBlock({ draw, block }: { draw: () => React.ReactNode; block: React.ReactNode }) {\n const node = draw();\n return node === null || node === undefined || node === false ? <>{block}</> : <div className=\"my-3\">{node}</div>;\n}\n\nconst MarkdownCode = ({ className, children }: { className?: string; children?: React.ReactNode }) => {\n const { copiedBlock, onCopyCode, codeBlockRenderers, streaming, t } = useContext(MarkdownRenderContext);\n // The whole info string's first word: `vega-lite`, `c++`, `objective-c`.\n const match = /(?:^|\\s)language-(\\S+)/.exec(className || '');\n const codeStr = String(children).replace(/\\n$/, '');\n const hostRenderer = match ? codeBlockRenderers?.[match[1].toLowerCase()] : undefined;\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 const block = (\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={() => onCopyCode(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 if (!hostRenderer) return block;\n return (\n <HostRenderBoundary label=\"A chat code block renderer\" resetKey={`${codeStr}\\u0000${streaming}`} fallback={<HostRenderFallback t={t} />}>\n <HostCodeBlock draw={() => hostRenderer(codeStr, { streaming })} block={block} />\n </HostRenderBoundary>\n );\n }\n return <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\nconst MarkdownLink = ({ href, children }: { href?: string; children?: React.ReactNode }) => {\n const { onRelativeLinkClick } = useContext(MarkdownRenderContext);\n const internalHref = toInternalHref(href);\n const routeInternally = internalHref !== null && !!onRelativeLinkClick;\n // A new tab unless the link was authored relative AND stays on this origin:\n // the second half keeps `/\\host` (which resolves off-site) out of this tab.\n const openInNewTab = !routeInternally && !(isRelativeHref(href) && internalHref !== null);\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\nconst MarkdownImage = ({ src, alt }: { src?: unknown; alt?: string }) => {\n const { requestHeaders, t } = useContext(MarkdownRenderContext);\n if (typeof src !== 'string' || !src) return null;\n // Keyed by its source: the image at a given place can change while the\n // message streams (a reference-style image resolves to each partial URL of\n // its definition as that line arrives), and a new source must not inherit the\n // previous one's fetched blob or its load error.\n return <ChatImage key={src} src={src} alt={alt || ''} requestHeaders={requestHeaders} t={t} />;\n};\n\nconst MARKDOWN_COMPONENTS: 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: MarkdownCode,\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: MarkdownLink,\n img: MarkdownImage,\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 }) => <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\nconst REMARK_PLUGINS = [remarkGfm, remarkBreaks];\n\n/** One parse of `source`, run again only when the text itself changes. */\nconst MarkdownBody = memo(({ source }: { source: string }) => (\n <Markdown remarkPlugins={REMARK_PLUGINS} urlTransform={markdownUrlTransform} components={MARKDOWN_COMPONENTS}>\n {source}\n </Markdown>\n));\n\nMarkdownBody.displayName = 'MarkdownBody';\n\n/**\n * A streamed message, one top-level block per parse: a settled block keeps its\n * rendering, and a new chunk re-parses only the blocks it can still change.\n * The blocks render as siblings, which is the markup one parse of the whole\n * text produces.\n */\nconst StreamedMarkdownBody = ({ source }: { source: string }) => {\n const [splitter] = useState(() => new MarkdownBlockSplitter());\n const blocks = useMemo(() => splitter.split(source), [splitter, source]);\n return (\n <>\n {blocks.map((block, index) => (\n <MarkdownBody key={index} source={block} />\n ))}\n </>\n );\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 — and only the blocks of it that can\n * still change.\n */\nexport const MarkdownMessage = memo(({ codeBlockRenderers, ...props }: MarkdownMessageProps) => {\n const { content, streaming = false, onRelativeLinkClick, requestHeaders, t = identity } = props;\n const [copiedBlock, setCopiedBlock] = useState<string | null>(null);\n\n // Rendered by blocks once it has streamed: switching to one parse when the\n // stream ends would rebuild the whole message to produce the same markup,\n // fetching its images again behind their placeholders and dropping a\n // selection in it. A message first rendered finished (a restored thread, the\n // panel reopened) takes one parse.\n const [byBlocks, setByBlocks] = useState(streaming);\n if (streaming && !byBlocks) setByBlocks(true);\n\n // Preprocess once per `content`: every pass scans the whole message, so\n // memoizing keeps that work off the hot path for large messages.\n const processedContent = useMemo(() => prepareMarkdown(content), [content]);\n\n const onCopyCode = useCallback((code: string) => {\n navigator.clipboard.writeText(code);\n setCopiedBlock(code);\n setTimeout(() => setCopiedBlock(null), 2000);\n }, []);\n\n const renderContext = useMemo(\n () => ({ copiedBlock, onCopyCode, onRelativeLinkClick, requestHeaders, codeBlockRenderers, streaming, t }),\n [copiedBlock, onCopyCode, onRelativeLinkClick, requestHeaders, codeBlockRenderers, streaming, t],\n );\n\n return (\n <MarkdownRenderContext.Provider value={renderContext}>\n {byBlocks ? <StreamedMarkdownBody source={processedContent} /> : <MarkdownBody source={processedContent} />}\n </MarkdownRenderContext.Provider>\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, translate } 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 ? translate(t, '{count} iterations', { count: iterations }) : '',\n totalCalls === 1 ? t('1 tool call') : translate(t, '{count} tool calls', { count: totalCalls }),\n transfers.length === 1 ? t('1 transfer') : transfers.length > 1 ? translate(t, '{count} transfers', { count: transfers.length }) : '',\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, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type {\n AgentStatusState,\n ChatMarkerRenderer,\n ChatCodeBlockRenderer,\n ChatArtifact,\n ChatArtifactKind,\n ChatAttachment,\n ChatConversationRef,\n ChatMessage,\n ChatMessageSlotContext,\n MessageFeedback,\n ToolApprovalDecision,\n ToolApprovalProposal,\n} from '../types';\nimport { feedbackKeyOf, useMessageFeedback, type FeedbackEntry } from '../hooks/useMessageFeedback';\nimport { useSpeechReader } from '../hooks/useSpeechReader';\nimport type { TtsRoute } from '../utils/speech';\nimport { answerMarkdownSources, artifactKey, splitAnswerMarkers, stripAnswerMarkers } from '../utils';\nimport { FEEDBACK_COMMENT_MAX_LENGTH, feedbackModeOf, shownFeedback, type FeedbackMode } from '../utils/feedback';\nimport { formatMessageTime } from '../utils/messageTime';\nimport {\n createScrollFollower,\n innerBoxScrollsUp,\n keyScrollsUp,\n nextTouchAnchor,\n pressTakesScrollbar,\n singleTouchY,\n swipeScrollsUp,\n wheelScrollsUp,\n} from '../utils/scrollFollow';\nimport { hasSpeakableWords } from '../utils/speech';\nimport {\n AlertTriangleIcon,\n CheckIcon,\n ChevronDownIcon,\n CopyIcon,\n DownloadIcon,\n FileIcon,\n InfoIcon,\n MessageSquareIcon,\n ThumbsDownIcon,\n ThumbsUpIcon,\n VolumeIcon,\n VolumeOffIcon,\n} from './icons';\nimport { ArtifactCard } from './ArtifactCard';\nimport { ChatApprovalPrompt } from './ChatApprovalPrompt';\nimport { ChatImage } from './ChatImage';\nimport { ChatThinking, type ThinkingOverrides } from './ChatThinking';\nimport { HostRenderBoundary, HostRenderFallback, HostRendered } from './HostRenderBoundary';\nimport { MarkdownMessage } from './MarkdownMessage';\nimport { ReasoningDetailsDialog } from './ReasoningDetailsDialog';\nimport { Tooltip } from './Tooltip';\nimport { askHost } from '../utils/askHost';\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\nconst parentOf = (node: Element) => node.parentElement;\nconst overflowYOf = (node: Element) => getComputedStyle(node).overflowY;\n\n/** Arrows and Page Up move the caret of a field (the feedback comment), not the thread. */\nfunction isTypingTarget(target: EventTarget): boolean {\n return target instanceof HTMLElement && (target.isContentEditable || !!target.closest('input, textarea, select'));\n}\n\nconst CONTROLS = 'a[href], button, summary, label, input, textarea, select, [role=\"button\"], [role=\"link\"]';\n\n/** A link, a button or a field: a middle click opens it, Shift+Space presses it. */\nfunction isControl(target: EventTarget): boolean {\n return target instanceof Element && !!target.closest(CONTROLS);\n}\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 /** Download a deck, report or video through the host's proxy (`ApiEndpoints.artifacts`). */\n onDownloadArtifact?: (artifact: ChatArtifact, format: string) => Promise<void>;\n /** Describe one again, to follow a video that is still rendering. */\n onRefreshArtifact?: (kind: ChatArtifactKind, id: string) => Promise<ChatArtifact | undefined>;\n /** Auth headers used when fetching previewed images (see `ChatImage`). */\n requestHeaders?: Record<string, string>;\n /** Host-level override for the waiting panel (messages and the invitation to play). */\n miniGameEnabled?: boolean;\n /** Opens the XTM One arcade in place; wins over `waitingGameUrl`. */\n onPlayWaitingGame?: () => void;\n /** Where the XTM One arcade is played, opened in a new tab. */\n waitingGameUrl?: string | null;\n /** Enables the 👍/👎 affordance on completed assistant messages (see `feedbackUrl`). */\n onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;\n /**\n * Where the panel stores ratings itself (`feedbackBaseUrl`): shows the thumbs\n * on every answer the backend identified, whether or not `onMessageFeedback`\n * is passed.\n */\n feedbackUrl?: string | null;\n /** The conversation on screen; its ratings are stored against it. */\n conversationId?: string | null;\n /** BCP 47 tag the times are formatted in and answers are read aloud in. */\n locale?: string;\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 /** Ends an agent's announced wait early (\"Continue now\"). */\n onSkipWait?: () => void;\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 /**\n * The conversations the panel can open, by id: the loaded conversation list.\n * A referenced conversation's chip opens it through `onOpenConversation`\n * when it is there, and is a plain label otherwise.\n */\n openableConversationIds?: ReadonlySet<string>;\n onOpenConversation?: (conversationId: string) => void;\n markerRenderers?: Record<string, ChatMarkerRenderer>;\n markerKindList?: readonly string[];\n /** The host's code block renderers by lower-cased language (see `ChatPanelProps.codeBlockRenderers`). */\n codeBlockRenderers?: Record<string, ChatCodeBlockRenderer>;\n transformMessageText?: (text: string, message: ChatMessage) => string;\n onArtifactOpen?: (artifact: ChatArtifact) => void;\n renderMessageExtras?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;\n messageFooterActions?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;\n /** Shown after the last message. */\n transcriptFooter?: React.ReactNode;\n /** The backend's voice for read aloud (`apiEndpoints.tts`). */\n ttsRoute?: TtsRoute;\n /** Fetches older messages from the backend; resolves to how many arrived. Omitted when there are none. */\n onLoadEarlier?: () => Promise<number>;\n /** The host's overrides of the status line and the waiting panel. */\n thinking?: ThinkingOverrides;\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/**\n * The footer's secondary controls show while the message is hovered or holds\n * the focus, and always where nothing hovers (a touch screen). A keyboard user\n * tabbing onto one sees it through `focus-visible` / `focus-within`.\n */\nconst SHOWN_ON_HOVER = 'group-hover/msg:opacity-100 group-focus-within/msg:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100';\nconst FOOTER_BUTTON = 'p-1 rounded-lg transition-opacity outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)]';\nconst ACCENT_ON_HOVER = 'hover:text-[var(--chat-accent)] focus-visible:text-[var(--chat-accent)]';\nconst REVEALED_BUTTON = `opacity-0 ${SHOWN_ON_HOVER} ${ACCENT_ON_HOVER}`;\n/**\n * The reasoning details button stays dimly in view, so a turn that reasoned is\n * noticed, and reads like its neighbours once they show.\n */\nconst DIMMED_BUTTON = `opacity-50 ${SHOWN_ON_HOVER} ${ACCENT_ON_HOVER}`;\n/** A control in its \"on\" state (the given rating, the message being read) stays in view. */\nconst ACTIVE_BUTTON = 'opacity-100 text-[var(--chat-accent)]';\n\n/** When the message was sent; the tooltip spells the date out. */\nconst MessageTime = ({ timestamp, locale }: { timestamp: Date; locale?: string }) => {\n const time = formatMessageTime(timestamp, locale);\n if (!time) return null;\n return (\n <Tooltip title={time.full}>\n <time dateTime={time.iso} className=\"px-1 text-[0.65rem] tabular-nums text-gray-400 dark:text-white/40 select-none\">\n <span aria-hidden=\"true\">{time.label}</span>\n <span className=\"sr-only\">{time.full}</span>\n </time>\n </Tooltip>\n );\n};\n\n/** Copies a message as plain text. Revealed on message hover. */\nconst MessageCopyButton = ({ text, label, t }: { text: string; label: 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 const name = copied ? t('Copied!') : label;\n return (\n <Tooltip title={name}>\n <button\n type=\"button\"\n onClick={handleCopy}\n aria-label={name}\n className={`${FOOTER_BUTTON} ${copied ? 'opacity-100 text-green-500 dark:text-green-400' : REVEALED_BUTTON}`}\n >\n {copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}\n </button>\n </Tooltip>\n );\n};\n\n/**\n * 👍/👎 on a completed answer. Clicking the active value clears it. A rating\n * given before the conversation was reopened is final: only it is shown.\n */\nconst MessageFeedbackButtons = ({\n value,\n locked,\n pending,\n onChange,\n downRef,\n t,\n}: {\n value: MessageFeedback | null;\n locked: boolean;\n pending: boolean;\n onChange: (next: MessageFeedback | null) => void;\n downRef: React.RefObject<HTMLButtonElement | null>;\n t: (key: string) => string;\n}) => {\n const lockedId = useId();\n const inert = locked || pending;\n\n const button = (target: MessageFeedback, label: string) => {\n const active = value === target;\n if (locked && !active) return null;\n const Icon = target === 'up' ? ThumbsUpIcon : ThumbsDownIcon;\n return (\n <Tooltip title={locked ? t('Feedback already submitted') : label}>\n <button\n ref={target === 'down' ? downRef : undefined}\n type=\"button\"\n onClick={() => {\n if (!inert) onChange(active ? null : target);\n }}\n aria-label={label}\n aria-pressed={active}\n // Not `disabled`: a disabled button takes no focus and no hover, so\n // it could not say why it does not answer.\n aria-disabled={inert || undefined}\n aria-describedby={locked ? lockedId : undefined}\n className={`${FOOTER_BUTTON} ${active ? ACTIVE_BUTTON : REVEALED_BUTTON} ${inert ? 'cursor-default' : ''}`}\n >\n <Icon size={14} filled={active} />\n </button>\n </Tooltip>\n );\n };\n\n return (\n <>\n {button('up', t('Good response'))}\n {button('down', t('Bad response'))}\n {locked && (\n <span id={lockedId} className=\"sr-only\">\n {t('Feedback already submitted')}\n </span>\n )}\n </>\n );\n};\n\n/** The optional word on what went wrong, asked once a thumbs down is stored. */\nconst FeedbackComment = ({\n pending,\n onSave,\n onSkip,\n t,\n}: {\n pending: boolean;\n onSave: (text: string) => void;\n onSkip: () => void;\n t: (key: string) => string;\n}) => {\n const [text, setText] = useState('');\n const id = useId();\n const canSave = !pending && !!text.trim();\n\n return (\n <div className=\"mt-1 flex w-full max-w-[90%] flex-col gap-1.5 rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/[0.04] p-2\">\n <label htmlFor={id} className=\"text-[0.7rem] text-gray-600 dark:text-white/60\">\n {t('Tell us what went wrong (optional)')}\n </label>\n <textarea\n id={id}\n // Opened by the user's own thumbs down, to be typed into.\n autoFocus\n rows={2}\n maxLength={FEEDBACK_COMMENT_MAX_LENGTH}\n value={text}\n // Read-only rather than disabled while saving, so the focus stays here.\n readOnly={pending}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n e.stopPropagation();\n if (!pending) onSkip();\n } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n if (canSave) onSave(text);\n }\n }}\n placeholder={t('What could be improved?')}\n className=\"w-full resize-y rounded-md border border-gray-200 dark:border-white/10 bg-white dark:bg-transparent px-2 py-1 text-[0.75rem] leading-5 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-white/30 outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)]\"\n />\n <div className=\"flex justify-end gap-1.5\">\n <button\n type=\"button\"\n onClick={() => {\n if (!pending) onSkip();\n }}\n aria-disabled={pending || undefined}\n className=\"rounded-md px-2 py-0.5 text-[0.7rem] text-gray-500 dark:text-white/50 transition-colors hover:text-[var(--chat-accent)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)]\"\n >\n {t('Skip')}\n </button>\n <button\n type=\"button\"\n onClick={() => {\n if (canSave) onSave(text);\n }}\n aria-disabled={!canSave || undefined}\n className={`rounded-md px-2 py-0.5 text-[0.7rem] text-white bg-[var(--chat-accent)] transition-opacity outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)] ${canSave ? 'hover:opacity-90' : 'opacity-50 cursor-default'}`}\n >\n {t('Save')}\n </button>\n </div>\n </div>\n );\n};\n\n/** Reads the answer aloud; the same button stops it. */\nconst ReadAloudButton = ({ speaking, onToggle, t }: { speaking: boolean; onToggle: () => void; t: (key: string) => string }) => {\n const name = speaking ? t('Stop reading') : t('Read aloud');\n return (\n <Tooltip title={name}>\n <button type=\"button\" onClick={onToggle} aria-label={name} className={`${FOOTER_BUTTON} ${speaking ? ACTIVE_BUTTON : REVEALED_BUTTON}`}>\n {speaking ? <VolumeOffIcon size={14} /> : <VolumeIcon size={14} />}\n </button>\n </Tooltip>\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 onDownloadArtifact?: (artifact: ChatArtifact, format: string) => Promise<void>;\n onRefreshArtifact?: (kind: ChatArtifactKind, id: string) => Promise<ChatArtifact | undefined>;\n requestHeaders?: Record<string, string>;\n /** How this answer's thumbs work, or null for none (`feedbackModeOf`). */\n feedbackMode: FeedbackMode | null;\n /** What the user did to its rating in this session. */\n feedback: FeedbackEntry | undefined;\n onRate: (msg: ChatMessage, next: MessageFeedback | null, current: FeedbackEntry | undefined) => void;\n onSaveComment: (msg: ChatMessage, text: string, current: FeedbackEntry) => void;\n onSkipComment: (msg: ChatMessage, current: FeedbackEntry) => void;\n locale?: string;\n /** The browser can read aloud. */\n canSpeak: boolean;\n /** This message is being read aloud. */\n isSpeaking: boolean;\n onToggleSpeech: (id: string, documents: readonly string[]) => void;\n /** Given only to a message referencing conversations (see `ChatMessagesProps`). */\n openableConversationIds?: ReadonlySet<string>;\n onOpenConversation?: (conversationId: string) => void;\n /** The host's marker cards by upper-cased name (see `ChatPanelProps.markerRenderers`). */\n markerRenderers?: Record<string, ChatMarkerRenderer>;\n /** Their names, as `markerKinds` normalizes them. */\n markerKindList?: readonly string[];\n codeBlockRenderers?: Record<string, ChatCodeBlockRenderer>;\n transformMessageText?: (text: string, message: ChatMessage) => string;\n onArtifactOpen?: (artifact: ChatArtifact) => void;\n renderMessageExtras?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;\n messageFooterActions?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;\n t: (key: string) => string;\n}\n\nconst NO_KINDS: readonly string[] = [];\nconst BUILT_IN_ARTIFACT_KINDS: ReadonlySet<string> = new Set(['PRESENTATION', 'REPORT', 'VIDEO']);\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 onDownloadArtifact,\n onRefreshArtifact,\n requestHeaders,\n feedbackMode,\n feedback,\n onRate,\n onSaveComment,\n onSkipComment,\n locale,\n canSpeak,\n isSpeaking,\n onToggleSpeech,\n openableConversationIds,\n onOpenConversation,\n markerRenderers,\n markerKindList = NO_KINDS,\n codeBlockRenderers,\n transformMessageText,\n onArtifactOpen,\n renderMessageExtras,\n messageFooterActions,\n t,\n }: MessageRowProps) => {\n const [showReasoning, setShowReasoning] = useState(false);\n const isAssistant = msg.role === 'assistant';\n // What the host shows of the text: also what is copied and read aloud.\n // A transform that throws shows the text as it came, never takes the thread down.\n const content = useMemo(() => askHost(transformMessageText, msg.content, msg) ?? msg.content, [transformMessageText, msg]);\n const isEmpty = !content;\n const downRef = useRef<HTMLButtonElement>(null);\n\n // The comment question takes the focus when it opens; when it closes with\n // the focus still in it (the element is gone, so the page lost it), the\n // focus goes back to the thumb that opened it rather than to the page.\n const commentOpen = !!feedback?.commentOpen;\n const wasCommentOpen = useRef(commentOpen);\n useLayoutEffect(() => {\n const closed = wasCommentOpen.current && !commentOpen;\n wasCommentOpen.current = commentOpen;\n if (closed && (!document.activeElement || document.activeElement === document.body)) downRef.current?.focus();\n }, [commentOpen]);\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 const renderArtifactCard = (artifact: ChatArtifact, key: string) => (\n <ArtifactCard key={key} artifact={artifact} t={t} onDownload={onDownloadArtifact} onRefresh={onRefreshArtifact} onOpen={onArtifactOpen} />\n );\n\n const renderHostMarker = (kind: string, args: string, key: string) => {\n const renderer = markerRenderers?.[kind];\n if (!renderer || (isStreaming && !renderer.renderWhileStreaming)) return null;\n return (\n <div key={key} className={renderer.fullWidth ? 'w-full' : 'w-full max-w-[90%]'}>\n <HostRenderBoundary label=\"A chat marker card\" resetKey={`${args}\\u0000${isStreaming}`} fallback={<HostRenderFallback t={t} />}>\n <HostRendered draw={() => renderer.render(args, { message: msg, streaming: isStreaming })} />\n </HostRenderBoundary>\n </div>\n );\n };\n\n // Render assistant content as an ordered interleave of prose segments and\n // cards, so a reply with markers like\n // `text [[FILE:a]] more text [[PRESENTATION:b]]` keeps the cards at their\n // source position. File cards only render when a download handler is wired\n // (`onDownloadFile`); a deck, report or video always renders, from its\n // descriptor. Attachments and artifacts whose marker isn't found in the\n // prose are appended as a fallback; a marker with no descriptor renders\n // nothing. During streaming neither is hydrated yet, so only prose (markers\n // stripped) renders.\n const buildAssistantBlocks = (): React.ReactNode[] => {\n const parts = splitAnswerMarkers(content, isStreaming, markerKindList);\n const attByFileId = new Map((msg.attachments ?? []).map((a) => [a.fileId, a] as const));\n const artifactsByKey = new Map((msg.artifacts ?? []).map((a) => [artifactKey(a.kind, a.id), a] as const));\n const used = new Set<string>();\n const usedArtifacts = 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\n content={part.value}\n streaming={isStreaming}\n onRelativeLinkClick={onRelativeLinkClick}\n requestHeaders={requestHeaders}\n codeBlockRenderers={codeBlockRenderers}\n t={t}\n />\n </div>,\n );\n }\n } else if (part.type === 'marker') {\n // A built-in kind the host draws itself is drawn once, by the host.\n if (part.kind === 'FILE') used.add(part.args);\n else if (BUILT_IN_ARTIFACT_KINDS.has(part.kind)) usedArtifacts.add(artifactKey(part.kind.toLowerCase(), part.args.split('#/')[0]));\n const card = renderHostMarker(part.kind, part.args, `m-${part.kind}-${i}`);\n if (card) blocks.push(card);\n } else if (part.type === 'artifact') {\n const key = artifactKey(part.kind, part.id);\n const artifact = artifactsByKey.get(key);\n if (artifact && !usedArtifacts.has(key)) {\n usedArtifacts.add(key);\n blocks.push(renderArtifactCard(artifact, `a-${key}-${i}`));\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 (msg.artifacts ?? []).forEach((artifact) => {\n const key = artifactKey(artifact.kind, artifact.id);\n if (!usedArtifacts.has(key)) {\n usedArtifacts.add(key);\n const kind = artifact.kind.toUpperCase();\n if (markerRenderers?.[kind]) {\n const card = renderHostMarker(kind, artifact.slideRef ? `${artifact.id}#/${artifact.slideRef}` : artifact.id, `orphan-a-${key}`);\n if (card) blocks.push(card);\n } else {\n blocks.push(renderArtifactCard(artifact, `orphan-a-${key}`));\n }\n }\n });\n\n (msg.attachments ?? []).forEach((att) => {\n if (used.has(att.fileId)) return;\n if (markerRenderers?.FILE) {\n const card = renderHostMarker('FILE', att.fileId, `orphan-${att.fileId}`);\n if (card) blocks.push(card);\n } else if (onDownloadFile) {\n blocks.push(renderAttachmentCard(att, `orphan-${att.fileId}`));\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 && !msg.stopped) {\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 // A conversation the message referenced with `@`: it opens in the panel\n // when the panel can open it, and otherwise just says what was referenced.\n const renderConversationRef = (ref: ChatConversationRef, i: number) => {\n const label = ref.title || (ref.key ? `@${ref.key}` : t('Untitled conversation'));\n const chip = 'inline-flex items-center gap-1 max-w-[240px] px-2 py-0.5 rounded-full border text-[0.7rem]';\n const content = (\n <>\n <MessageSquareIcon size={13} className=\"shrink-0\" />\n <span className=\"truncate\">{label}</span>\n </>\n );\n if (onOpenConversation && openableConversationIds?.has(ref.conversationId)) {\n return (\n <Tooltip key={`ref-${ref.conversationId}-${i}`} title={t('Open conversation')}>\n <button\n type=\"button\"\n onClick={() => onOpenConversation(ref.conversationId)}\n className={`${chip} border-[var(--chat-accent-40)] text-[var(--chat-accent)] hover:bg-[var(--chat-accent)]/10 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--chat-accent-50)]`}\n >\n {content}\n </button>\n </Tooltip>\n );\n }\n return (\n <Tooltip key={`ref-${ref.conversationId}-${i}`} title={ref.key ? `@${ref.key}` : ''}>\n <span className={`${chip} border-gray-200 dark:border-white/10 text-gray-600 dark:text-white/60`}>{content}</span>\n </Tooltip>\n );\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 const plainText = stripAnswerMarkers(content, markerKindList);\n const speechDocuments = useMemo(\n () => (showActions && canSpeak ? answerMarkdownSources(content, markerKindList) : null),\n [showActions, canSpeak, content, markerKindList],\n );\n const shown = shownFeedback(feedback, msg.feedback);\n const speechKey = feedbackKeyOf(msg);\n\n return (\n <div data-message-id={msg.serverId ?? msg.id} 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 && (msg.conversationRefs?.length ?? 0) > 0 && (\n <div className=\"flex gap-1.5 flex-wrap mb-1.5 justify-end max-w-[90%]\">{msg.conversationRefs!.map(renderConversationRef)}</div>\n )}\n\n {isAssistant ? (\n <div data-testid=\"filigran-chat-assistant-message\" className=\"flex flex-col gap-1.5 w-full items-start\">\n {buildAssistantBlocks()}\n {msg.stopped && !isStreaming && <p className=\"pl-1 text-[0.75rem] italic text-gray-400 dark:text-white/40\">{t('Stopped')}</p>}\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\n data-testid=\"filigran-chat-user-message\"\n 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 >\n {content}\n </div>\n )}\n\n {renderMessageExtras && (\n <div className={`w-full ${isAssistant ? '' : 'flex justify-end'}`}>{renderMessageExtras(msg, { streaming: isStreaming })}</div>\n )}\n\n {/* The user's side mirrors the answer's: the time hugs the bubble's edge. */}\n {!isAssistant && (\n <div className=\"mt-0.5 flex items-center justify-end gap-0.5 text-gray-400 dark:text-white/40\">\n {!!content.trim() && <MessageCopyButton text={content} label={t('Copy message')} t={t} />}\n {!msg.timestampUnknown && <MessageTime timestamp={msg.timestamp} locale={locale} />}\n </div>\n )}\n\n {showActions && (\n <div className=\"mt-0.5 flex flex-wrap items-center gap-0.5 text-gray-400 dark:text-white/40\">\n {!msg.timestampUnknown && <MessageTime timestamp={msg.timestamp} locale={locale} />}\n {hasReasoningDetails && (\n <Tooltip title={msg.isTruncated ? t('Reasoning details — turn limit reached') : t('Reasoning details')}>\n <button\n type=\"button\"\n onClick={() => setShowReasoning((v) => !v)}\n className={`${FOOTER_BUTTON} ${\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 : DIMMED_BUTTON\n }`}\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 </Tooltip>\n )}\n <MessageCopyButton text={plainText} label={t('Copy response')} t={t} />\n {feedbackMode && (\n <MessageFeedbackButtons\n value={shown.value}\n locked={shown.locked}\n pending={!!feedback?.pending}\n onChange={(next) => onRate(msg, next, feedback)}\n downRef={downRef}\n t={t}\n />\n )}\n {speechDocuments && hasSpeakableWords(speechDocuments) && (\n <ReadAloudButton speaking={isSpeaking} onToggle={() => onToggleSpeech(speechKey, speechDocuments)} t={t} />\n )}\n {messageFooterActions?.(msg, { streaming: isStreaming })}\n </div>\n )}\n {showActions && feedback?.commentOpen && (\n <FeedbackComment\n pending={feedback.pending}\n onSave={(text) => onSaveComment(msg, text, feedback)}\n onSkip={() => onSkipComment(msg, feedback)}\n t={t}\n />\n )}\n {showActions && feedback?.error && (\n <p role=\"alert\" className=\"mt-0.5 pl-1 text-[0.7rem] text-red-600 dark:text-red-400\">\n {feedback.error}\n </p>\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 onDownloadArtifact,\n onRefreshArtifact,\n requestHeaders,\n miniGameEnabled = true,\n onPlayWaitingGame,\n waitingGameUrl,\n onMessageFeedback,\n isResumingAfterDecision,\n onSkipWait,\n pendingApprovals,\n onSubmitApprovalDecisions,\n isSubmittingApproval,\n approvalError,\n feedbackUrl,\n conversationId,\n locale,\n openableConversationIds,\n onOpenConversation,\n markerRenderers,\n markerKindList,\n codeBlockRenderers,\n transformMessageText,\n onArtifactOpen,\n renderMessageExtras,\n messageFooterActions,\n transcriptFooter,\n onLoadEarlier,\n ttsRoute,\n thinking,\n t,\n}: ChatMessagesProps) => {\n const scrollRef = useRef<HTMLDivElement>(null);\n const [renderWindow, setRenderWindow] = useState(INITIAL_RENDER_WINDOW);\n const {\n entries: feedbackEntries,\n rate,\n saveComment,\n skipComment,\n } = useMessageFeedback({\n feedbackUrl,\n conversationId,\n requestHeaders,\n onMessageFeedback,\n t,\n });\n const speech = useSpeechReader(locale, ttsRoute);\n\n // Keeps the end of the conversation in view while the reader is at the\n // bottom (see `createScrollFollower`). It holds no React state: its timer\n // firing after unmount finds no container and does nothing.\n const [follower] = useState(() => createScrollFollower(() => scrollRef.current));\n // The highest point of the touch in progress: a swipe is measured from it,\n // so a finger that turns back down counts from where it turned.\n const touchAnchorRef = useRef<number | null>(null);\n\n // A gesture toward the top is the reader leaving the bottom — but only where\n // it moves the thread itself: an overlay portalled out of it (the image\n // lightbox) still bubbles its events here through React and scrolls nothing\n // of ours, and a box inside it that can still go up takes the gesture first.\n const leaveFrom = (container: HTMLDivElement, target: EventTarget) => {\n if (!(target instanceof Element) || !container.contains(target)) return;\n if (innerBoxScrollsUp<Element>(target, container, parentOf, overflowYOf)) return;\n follower.leave();\n };\n const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {\n if (wheelScrollsUp(event)) leaveFrom(event.currentTarget, event.target);\n };\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.defaultPrevented || !keyScrollsUp(event) || isTypingTarget(event.target)) return;\n if (event.key === ' ' && isControl(event.target)) return;\n leaveFrom(event.currentTarget, event.target);\n };\n const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n const container = event.currentTarget;\n const rect = container.getBoundingClientRect();\n const press = {\n button: event.button,\n onContainer: event.target === container,\n onControl: isControl(event.target),\n x: event.clientX - rect.left - container.clientLeft,\n y: event.clientY - rect.top - container.clientTop,\n clientWidth: container.clientWidth,\n clientHeight: container.clientHeight,\n scrollTop: container.scrollTop,\n scrollHeight: container.scrollHeight,\n };\n if (pressTakesScrollbar(press)) leaveFrom(container, event.target);\n };\n // A second finger ends the swipe: a pinch is no scroll, and the swipe of\n // the finger left on the screen is measured afresh from its next move.\n const handleTouchStart = (event: React.TouchEvent) => {\n touchAnchorRef.current = singleTouchY(event.touches);\n };\n const handleTouchMove = (event: React.TouchEvent<HTMLDivElement>) => {\n const y = singleTouchY(event.touches);\n const anchor = touchAnchorRef.current;\n if (y !== null && anchor !== null && swipeScrollsUp(anchor, y)) leaveFrom(event.currentTarget, event.target);\n touchAnchorRef.current = nextTouchAnchor(anchor, y);\n };\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 // Older messages fetched from the backend land above the thread: they widen\n // the window and leave the reader where they were, unlike a switch. Both\n // happen before paint, so the thread never shows a jump.\n const prependedRef = useRef<{\n firstId: string | undefined;\n visible: number;\n count: number;\n scrollHeight: number;\n scrollTop: number;\n } | null>(null);\n const [loadingEarlier, setLoadingEarlier] = useState(false);\n useLayoutEffect(() => {\n const prepended = prependedRef.current;\n // Counted from where the old first message landed: one appended meanwhile is no older one.\n const landed = prepended ? messages.findIndex((m) => m.id === prepended.firstId) : -1;\n if (prepended && landed > 0) {\n prepended.count = landed;\n setRenderWindow(prepended.visible + prepended.count);\n return;\n }\n setRenderWindow(INITIAL_RENDER_WINDOW);\n }, [firstMessageId]);\n useLayoutEffect(() => {\n const prepended = prependedRef.current;\n if (!prepended?.count || renderWindow !== prepended.visible + prepended.count) return;\n prependedRef.current = null;\n const el = scrollRef.current;\n if (el) el.scrollTop = prepended.scrollTop + (el.scrollHeight - prepended.scrollHeight);\n });\n const showEarlier = async () => {\n if (messages.length > renderWindow) {\n setRenderWindow((w) => w + RENDER_WINDOW_STEP);\n return;\n }\n if (!onLoadEarlier || loadingEarlier) return;\n const el = scrollRef.current;\n setLoadingEarlier(true);\n prependedRef.current = {\n firstId: messages[0]?.id,\n visible: Math.min(messages.length, renderWindow),\n count: 0,\n scrollHeight: el?.scrollHeight ?? 0,\n scrollTop: el?.scrollTop ?? 0,\n };\n try {\n if ((await onLoadEarlier()) === 0) prependedRef.current = null;\n } finally {\n setLoadingEarlier(false);\n }\n };\n\n // A message the user just sent (a steer included) and a conversation just\n // opened are scrolled into view smoothly, and the view follows again.\n // Anything else — mostly the streamed answer, which changes `messages` on\n // every frame — keeps the bottom in view only while the view follows, so a\n // reader who scrolled up stays where they are.\n let userMessageCount = 0;\n for (const m of messages) if (m.role === 'user') userMessageCount += 1;\n const seenRef = useRef<{ firstMessageId?: string; userMessageCount: number } | null>(null);\n useLayoutEffect(() => {\n const seen = seenRef.current;\n seenRef.current = { firstMessageId, userMessageCount };\n // Older messages are not a switch: the reader stays where they were (above).\n if (prependedRef.current?.count) return;\n if (!seen || seen.firstMessageId !== firstMessageId || userMessageCount > seen.userMessageCount) {\n follower.reveal();\n } else {\n follower.keepUp();\n }\n }, [messages, firstMessageId, userMessageCount, follower]);\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.\n const thinkingLen = agentStatus?.thinkingContent?.length ?? 0;\n useLayoutEffect(() => {\n if (thinkingLen) follower.keepUp();\n }, [thinkingLen, follower]);\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 // Reading aloud is about the message on screen: it stops when the user\n // sends a new one, opens another conversation, or the message goes away.\n const { speakingId, stop: stopSpeech } = speech;\n const spokenStillShown = speakingId === null || messages.some((m) => feedbackKeyOf(m) === speakingId);\n const speechScope = useRef({ conversationId, userMessageCount });\n useEffect(() => {\n const previous = speechScope.current;\n speechScope.current = { conversationId, userMessageCount };\n if (previous.conversationId !== conversationId || userMessageCount > previous.userMessageCount || !spokenStillShown) stopSpeech();\n }, [conversationId, userMessageCount, spokenStillShown, stopSpeech]);\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 // A tab stop of its own, so the keys scroll the thread (and are read as\n // leaving the bottom) without first focusing something inside it.\n return (\n <div\n ref={scrollRef}\n role=\"region\"\n aria-label={t('Conversation transcript')}\n tabIndex={0}\n onScroll={follower.onScroll}\n onWheel={handleWheel}\n onKeyDown={handleKeyDown}\n onPointerDown={handlePointerDown}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n data-testid=\"filigran-chat-transcript\"\n className=\"flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-4 filigran-chat-scrollable outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--chat-accent-50)]\"\n >\n {(hasEarlierMessages || onLoadEarlier) && (\n <div className=\"flex justify-center\">\n <button\n type=\"button\"\n disabled={loadingEarlier}\n onClick={() => void showEarlier()}\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\n agentStatus={agentStatus}\n logoIcon={logoIcon}\n t={t}\n miniGameEnabled={miniGameEnabled}\n onPlayWaitingGame={onPlayWaitingGame}\n waitingGameUrl={waitingGameUrl}\n keepEndInView={follower.keepUp}\n onSkipWait={onSkipWait}\n {...thinking}\n />\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 onDownloadArtifact={onDownloadArtifact}\n onRefreshArtifact={onRefreshArtifact}\n requestHeaders={requestHeaders}\n feedbackMode={msg.role === 'assistant' ? feedbackModeOf(msg.serverId, feedbackUrl, conversationId, !!onMessageFeedback) : null}\n feedback={feedbackEntries[feedbackKeyOf(msg)]}\n onRate={rate}\n onSaveComment={saveComment}\n onSkipComment={skipComment}\n locale={locale}\n canSpeak={speech.supported}\n isSpeaking={speakingId === feedbackKeyOf(msg)}\n onToggleSpeech={speech.toggle}\n // Only to a row that needs them: a new list of openable\n // conversations must not re-render every row of the thread.\n openableConversationIds={msg.conversationRefs?.length ? openableConversationIds : undefined}\n onOpenConversation={msg.conversationRefs?.length ? onOpenConversation : undefined}\n markerRenderers={markerRenderers}\n markerKindList={markerKindList}\n codeBlockRenderers={codeBlockRenderers}\n transformMessageText={transformMessageText}\n onArtifactOpen={onArtifactOpen}\n renderMessageExtras={renderMessageExtras}\n messageFooterActions={messageFooterActions}\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\n agentStatus={agentStatus}\n logoIcon={logoIcon}\n t={t}\n miniGameEnabled={miniGameEnabled}\n onPlayWaitingGame={onPlayWaitingGame}\n waitingGameUrl={waitingGameUrl}\n keepEndInView={follower.keepUp}\n {...thinking}\n />\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 {transcriptFooter}\n {/* Holds the list's gap below its last item. */}\n <div />\n </div>\n );\n};\n","import { useEffect, useRef, useState } from 'react';\nimport type { Translate } from '../types';\nimport type { VoiceCallSnapshot } from '../hooks/voiceCallController';\nimport { MicIcon, MicOffIcon, PhoneOffIcon, VolumeOffIcon } from './icons';\n\n/** A transcript line longer than this is cut: the thread under the call holds it whole. */\nconst LINE_MAX_LENGTH = 200;\n\ninterface ChatVoiceCallProps {\n call: VoiceCallSnapshot;\n agentName: string;\n logoIcon: React.ReactNode;\n /** The status line of the turn the question waits on. */\n processingLabel: string;\n onInterrupt: () => void;\n onToggleMute: () => void;\n onEnd: () => void;\n t: Translate;\n}\n\nfunction formatDuration(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = seconds % 60;\n return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;\n}\n\n/**\n * The voice call, over the panel: who is on the call, what it is doing, what\n * was said, and the mute, end and skip controls. Tapping the call stops the\n * answer being said, or cancels the question awaited.\n */\nexport const ChatVoiceCall = ({ call, agentName, logoIcon, processingLabel, onInterrupt, onToggleMute, onEnd, t }: ChatVoiceCallProps) => {\n const [seconds, setSeconds] = useState(0);\n useEffect(() => {\n const id = window.setInterval(() => setSeconds((s) => s + 1), 1000);\n return () => window.clearInterval(id);\n }, []);\n const endRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n endRef.current?.scrollIntoView?.({ block: 'end' });\n }, [call.lines.length, call.heard]);\n\n const { phase, muted } = call;\n const label =\n phase === 'listening'\n ? muted\n ? t('Muted')\n : t('Listening...')\n : phase === 'processing'\n ? processingLabel\n : phase === 'speaking'\n ? t('Speaking…')\n : phase === 'error'\n ? t('Failed to get a response. Resuming…')\n : '';\n const tappable = phase === 'speaking' || phase === 'processing';\n const live = phase === 'speaking' || (phase === 'listening' && !muted);\n const roundButton = 'w-12 h-12 rounded-full flex items-center justify-center border transition-colors disabled:opacity-40';\n\n return (\n <div\n role=\"dialog\"\n aria-label={t('Voice call')}\n data-testid=\"filigran-chat-voice-call\"\n className=\"absolute inset-0 z-40 flex flex-col rounded-[inherit] bg-white dark:bg-[#1e1e2e]\"\n >\n <div className=\"flex items-center justify-between px-4 py-3 text-[0.7rem] uppercase tracking-[1px] text-gray-500 dark:text-white/50\">\n <span>{t('Voice call')}</span>\n <span className=\"font-mono normal-case tracking-normal\">{formatDuration(seconds)}</span>\n </div>\n\n <div className=\"flex flex-1 min-h-0 flex-col items-center justify-center gap-1 px-4\">\n <button\n type=\"button\"\n onClick={tappable ? onInterrupt : undefined}\n disabled={!tappable}\n aria-label={phase === 'speaking' ? t('Skip response') : phase === 'processing' ? t('Cancel question') : agentName}\n className={`relative mb-4 flex h-28 w-28 items-center justify-center rounded-full border-2 transition-colors ${\n phase === 'error' ? 'border-amber-400/50' : live ? 'border-[var(--chat-accent)]' : 'border-[var(--chat-accent-40)]'\n } ${tappable ? 'cursor-pointer' : 'cursor-default'}`}\n >\n {live && <span className=\"absolute inset-0 rounded-full border border-[var(--chat-accent-40)] animate-ping\" aria-hidden=\"true\" />}\n {phase === 'processing' && (\n <span className=\"absolute inset-3 rounded-full border-2 border-dashed border-[var(--chat-accent-40)] animate-spin\" aria-hidden=\"true\" />\n )}\n <span className=\"text-[var(--chat-accent)] [&>svg]:w-10 [&>svg]:h-10\">{logoIcon}</span>\n </button>\n <h2 className=\"max-w-full truncate text-base font-semibold text-gray-900 dark:text-white\">{agentName}</h2>\n <p aria-live=\"polite\" className=\"text-center text-[0.8125rem] text-gray-500 dark:text-white/60\">\n {label}\n </p>\n {phase === 'speaking' && <p className=\"text-[0.7rem] text-gray-400 dark:text-white/40\">{t('Speak or tap to interrupt')}</p>}\n {phase === 'processing' && <p className=\"text-[0.7rem] text-gray-400 dark:text-white/40\">{t('Tap to cancel')}</p>}\n {phase === 'listening' && call.heard && (\n <p className=\"mt-3 max-w-md text-center text-[0.8125rem] italic text-gray-500 dark:text-white/50\">“{call.heard}”</p>\n )}\n </div>\n\n {call.lines.length > 0 && (\n <div className=\"max-h-[35%] overflow-y-auto filigran-chat-scrollable px-4 pb-2\">\n <div className=\"flex flex-col gap-2\">\n {call.lines.map((line, i) => (\n <div key={i} className={`flex ${line.role === 'user' ? 'justify-end' : 'justify-start'}`}>\n <div\n className={`max-w-[85%] rounded-lg px-3 py-1.5 text-[0.75rem] leading-5 ${\n line.role === 'user'\n ? 'bg-[var(--chat-accent)]/10 text-gray-800 dark:text-white/80'\n : 'bg-gray-100 dark:bg-white/[0.06] text-gray-700 dark:text-white/70'\n }`}\n >\n {line.agent && (\n <div className=\"mb-0.5 text-[0.7rem] font-medium text-[var(--chat-accent)]\">\n {line.agent.name}\n {line.agent.failed && <span className=\"font-normal text-gray-500 dark:text-white/50\"> · {t('Did not answer')}</span>}\n </div>\n )}\n {line.content.length > LINE_MAX_LENGTH ? `${line.content.slice(0, LINE_MAX_LENGTH)}…` : line.content}\n </div>\n </div>\n ))}\n <div ref={endRef} />\n </div>\n </div>\n )}\n\n <div className=\"flex items-center justify-center gap-5 px-4 pb-5 pt-3\">\n <button\n type=\"button\"\n onClick={onToggleMute}\n aria-label={muted ? t('Unmute microphone') : t('Mute microphone')}\n aria-pressed={muted}\n className={`${roundButton} ${\n muted\n ? 'border-red-400/40 bg-red-500/10 text-red-500'\n : 'border-gray-200 dark:border-white/10 text-gray-600 dark:text-white/70 hover:bg-gray-100 dark:hover:bg-white/[0.06]'\n }`}\n >\n {muted ? <MicOffIcon size={20} /> : <MicIcon size={20} />}\n </button>\n <button\n type=\"button\"\n onClick={onEnd}\n aria-label={t('End voice call')}\n className=\"w-14 h-14 rounded-full flex items-center justify-center bg-red-500 text-white hover:bg-red-600 transition-colors\"\n >\n <PhoneOffIcon size={22} />\n </button>\n <button\n type=\"button\"\n onClick={onInterrupt}\n disabled={phase !== 'speaking'}\n aria-label={t('Skip response')}\n className={`${roundButton} border-gray-200 dark:border-white/10 text-gray-600 dark:text-white/70 hover:bg-gray-100 dark:hover:bg-white/[0.06]`}\n >\n <VolumeOffIcon size={20} />\n </button>\n </div>\n </div>\n );\n};\n","import type { Translate } from '../types';\nimport { translate, translateAround } from '../utils';\n\n/**\n * The greeting, as one sentence the locale owns end to end. The agent's name is\n * markup (it carries the accent colour), so it cannot be spliced into a string —\n * hence the split around its slot rather than three separate fragments.\n */\nconst Greeting = ({ firstName, agentName, t }: { firstName: string; agentName?: string; t: Translate }) => {\n if (!agentName) return <>{translate(t, 'How can I help you, {name}?', { name: firstName })}</>;\n const { before, after, hasSlot } = translateAround(t, 'How can {agent} help you, {name}?', 'agent', { name: firstName });\n return (\n <>\n {before}\n {hasSlot && <span className=\"text-[var(--chat-accent)]\">{agentName}</span>}\n {after}\n </>\n );\n};\n\ninterface ChatWelcomeProps {\n firstName: string;\n logoIcon: React.ReactNode;\n /** Already translated by the caller — see `ChatPanel`. */\n promptSuggestions: string[];\n /** Left out when nothing can be sent: the suggestions are not offered then. */\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: Translate;\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 <Greeting firstName={firstName} agentName={agentName} t={t} />\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 {/* A host that passes no suggestions gets no section: the heading on its own\n used to sit above an empty gap. */}\n {(suggestionsLoading || promptSuggestions.length > 0) && (\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 onPromptClick &&\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 {prompt}\n </button>\n ))\n )}\n </div>\n )}\n </div>\n);\n","import { useEffect, useMemo, useRef, useState, type DragEvent, type ReactNode } from 'react';\nimport type { ChatConversationSummary, ChatWorkspace } from '../types';\nimport type { WorkspaceWriteResult } from '../hooks/useWorkspaces';\nimport { timeAgo } from '../utils';\nimport { readStorage, STORAGE_KEYS, storageKey, writeStorage } from '../utils/storage';\nimport { fileableWorkspaces, groupCanToggle, groupConversations, groupIsOpen, isNoopMove, unfiledEmptyNote } from '../utils/workspaces';\nimport { ClearAllConversations } from './ClearAllConversations';\nimport { Dropdown } from './Dropdown';\nimport {\n BotIcon,\n CheckIcon,\n ChevronRightIcon,\n CloseIcon,\n EditIcon,\n FolderIcon,\n FolderInputIcon,\n FolderPlusIcon,\n PencilIcon,\n SearchIcon,\n SidebarIcon,\n TrashIcon,\n} from './icons';\nimport { Spinner } from './Spinner';\nimport { Tooltip } from './Tooltip';\n\n/**\n * The workspaces the list is grouped by, and what the list may do with them.\n * Omitted when the host does not name `apiEndpoints.workspaces` or the backend\n * refuses the list: the sidebar is then the flat list it always was.\n */\nexport interface SidebarWorkspaces {\n list: ChatWorkspace[];\n /** The workspace the next conversation will be created in, if any. */\n pendingWorkspaceId: string | null;\n onCreate: (name: string) => Promise<WorkspaceWriteResult>;\n onRename: (id: string, name: string) => Promise<WorkspaceWriteResult>;\n onDelete: (id: string) => Promise<WorkspaceWriteResult>;\n /** Resolves to `null` on success, or the backend's reason. */\n onMove: (conversationId: string, workspaceId: string | null) => Promise<string | null>;\n onNewChat: (workspaceId: string) => void;\n}\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 workspaces?: SidebarWorkspaces;\n /** Namespace of the browser-storage keys (see `storageKeyPrefix` on the panel). */\n storageKeyPrefix?: string;\n /** The host's controls on a row, before rename and delete. */\n rowActions?: (conversation: ChatConversationSummary) => ReactNode;\n /** The host's controls beside New conversation. */\n headerActions?: ReactNode;\n /** The host's sections under the list. */\n sections?: ReactNode;\n /** Deletes every conversation, after a confirmation; omitted when the host offers no route. */\n onClearAll?: () => 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/** What a dragged conversation row carries. */\nconst DRAG_TYPE = 'application/x-filigran-conversation';\nfunction readCollapsed(key: string): Set<string> {\n try {\n const parsed: unknown = JSON.parse(readStorage('local', key) ?? '[]');\n return new Set(Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === 'string') : []);\n } catch {\n return new Set();\n }\n}\n\nconst iconButton =\n 'p-1 rounded-md text-gray-400 dark:text-white/30 hover:bg-gray-200/60 dark:hover:bg-white/10 transition-colors disabled:opacity-40 disabled:pointer-events-none';\nconst sectionLabel = 'text-[0.625rem] font-medium uppercase tracking-wide text-gray-400 dark:text-white/40';\n\n/**\n * Persistent conversation list for fullscreen mode, mirroring the XTM One web\n * chat's sidebar - grouped by workspace when the host offers workspaces, the\n * way a desktop assistant groups its projects: collapsible groups, a\n * conversation started inside one, and rows moved between them from their\n * menu or by dragging them onto a group.\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 workspaces,\n storageKeyPrefix,\n rowActions,\n headerActions,\n sections,\n onClearAll,\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 // Where the collapsed workspace groups are remembered, per browser.\n const collapsedKey = storageKey(storageKeyPrefix, STORAGE_KEYS.collapsedWorkspaces);\n const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => readCollapsed(collapsedKey));\n const [creatingWorkspace, setCreatingWorkspace] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // A refusal is said, then goes: it is about the action just tried.\n useEffect(() => {\n if (!error) return;\n const timer = setTimeout(() => setError(null), 6000);\n return () => clearTimeout(timer);\n }, [error]);\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 toggleGroup = (key: string) => {\n setCollapsedGroups((prev) => {\n const next = new Set(prev);\n if (next.has(key)) next.delete(key);\n else next.add(key);\n writeStorage('local', collapsedKey, JSON.stringify([...next]));\n return next;\n });\n };\n\n const say = (reason: string | null, fallback: string) => setError(reason || t(fallback));\n\n const move = async (conversationId: string, workspaceId: string | null) => {\n // A drop on the group the row is already in (or an unfiled row on \"Not in\n // a workspace\") is no move: the row menu disables it, the drop ignores it.\n if (!workspaces || isNoopMove(conversations, conversationId, workspaceId)) return;\n const refusal = await workspaces.onMove(conversationId, workspaceId);\n if (refusal !== null) say(refusal, 'The conversation could not be moved');\n };\n\n const q = query.trim().toLowerCase();\n const matches = useMemo(() => (q ? (c: ChatConversationSummary) => (c.title || '').toLowerCase().includes(q) : undefined), [q]);\n const filtered = useMemo(() => (matches ? conversations.filter(matches) : conversations), [conversations, matches]);\n const grouped = useMemo(\n () => (workspaces ? groupConversations(conversations, workspaces.list, { matches }) : null),\n [conversations, workspaces, matches],\n );\n const fileable = workspaces ? fileableWorkspaces(workspaces.list) : [];\n const unfiledNote = grouped ? unfiledEmptyNote({ total: conversations.length, unfiled: grouped.unfiled.length, loading, searching: !!q }) : null;\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 const renderRow = (c: ChatConversationSummary) => (\n <ConversationRow\n key={c.conversationId}\n conversation={c}\n isActive={c.conversationId === activeConversationId}\n isEditing={c.conversationId === editingId}\n draftTitle={draftTitle}\n onDraftTitleChange={setDraftTitle}\n onCommitRename={commitRename}\n onCancelRename={cancelRename}\n onStartRename={onRename ? startRename : undefined}\n onSelect={onSelect}\n onDelete={onDelete}\n fileable={workspaces ? fileable : undefined}\n onMove={move}\n actions={rowActions}\n t={t}\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 {headerActions}\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\n {grouped && workspaces ? (\n <>\n {!q && (\n <div className=\"flex items-center justify-between px-2 pb-1 pt-1\">\n <span className={sectionLabel}>{t('Workspaces')}</span>\n <Tooltip title={t('New workspace')}>\n <button type=\"button\" onClick={() => setCreatingWorkspace(true)} aria-label={t('New workspace')} className={iconButton}>\n <FolderPlusIcon size={14} />\n </button>\n </Tooltip>\n </div>\n )}\n {creatingWorkspace && (\n <NameField\n placeholder={t('Workspace name')}\n onSubmit={async (name) => {\n setCreatingWorkspace(false);\n if (!name) return;\n const result = await workspaces.onCreate(name);\n if (!result.ok) say(result.error, 'The workspace could not be created');\n }}\n onCancel={() => setCreatingWorkspace(false)}\n />\n )}\n {grouped.groups.map((group) => (\n <WorkspaceGroupSection\n key={group.key}\n workspace={group.workspace}\n count={group.conversations.length}\n open={groupIsOpen(group.key, collapsedGroups, group, activeConversationId, !!q)}\n toggleable={groupCanToggle(group, activeConversationId, !!q)}\n pending={!!group.workspace && workspaces.pendingWorkspaceId === group.workspace.id && !activeConversationId}\n onToggle={() => toggleGroup(group.key)}\n onNewChat={workspaces.onNewChat}\n onRename={async (id, name) => {\n const result = await workspaces.onRename(id, name);\n if (!result.ok) say(result.error, 'The workspace could not be renamed');\n }}\n onDelete={async (id) => {\n const result = await workspaces.onDelete(id);\n if (!result.ok) say(result.error, 'The workspace could not be deleted');\n }}\n onDropConversation={(conversationId) => group.workspace && void move(conversationId, group.workspace.id)}\n t={t}\n >\n {group.conversations.length > 0 ? (\n group.conversations.map(renderRow)\n ) : (\n <p className=\"px-3 py-1.5 text-[0.65rem] text-gray-400 dark:text-white/30\">{t('Drop a conversation here, or start one with +.')}</p>\n )}\n </WorkspaceGroupSection>\n ))}\n <DropZone onDropConversation={(conversationId) => void move(conversationId, null)} accepts>\n <div className=\"flex items-center justify-between px-2 pb-1 pt-3\">\n <span className={sectionLabel}>{t('Not in a workspace')}</span>\n <span className=\"text-[0.625rem] tabular-nums text-gray-400 dark:text-white/30\">{grouped.unfiled.length}</span>\n </div>\n {grouped.unfiled.map(renderRow)}\n {unfiledNote && (\n <p className=\"px-3 py-1.5 text-[0.65rem] text-gray-400 dark:text-white/30\">\n {unfiledNote === 'none' ? t('No conversations yet') : t('Every conversation is in a workspace.')}\n </p>\n )}\n </DropZone>\n {q && grouped.groups.length === 0 && grouped.unfiled.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 ) : (\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 {filtered.map(renderRow)}\n </>\n )}\n {sections}\n {onClearAll && conversations.length > 0 && !q && (\n <ClearAllConversations onClear={onClearAll} className=\"px-2 pt-3\" iconButtonClass={iconButton} t={t} />\n )}\n </div>\n\n {error && (\n <p role=\"alert\" className=\"mx-3 mb-3 rounded-md bg-red-500/10 px-2.5 py-1.5 text-[0.7rem] text-red-600 dark:text-red-400\">\n {error}\n </p>\n )}\n </div>\n );\n};\n\n/** An element conversations can be dropped onto. */\nconst DropZone = ({\n onDropConversation,\n accepts,\n children,\n className = '',\n}: {\n onDropConversation: (conversationId: string) => void;\n accepts: boolean;\n children: ReactNode;\n className?: string;\n}) => {\n const [over, setOver] = useState(false);\n const depth = useRef(0);\n const carries = (e: DragEvent) => e.dataTransfer.types.includes(DRAG_TYPE);\n return (\n <div\n className={`rounded-lg transition-colors ${over ? 'bg-[var(--chat-accent)]/10 ring-1 ring-[var(--chat-accent)]/40' : ''} ${className}`}\n {...(accepts\n ? {\n onDragEnter: (e: DragEvent) => {\n if (!carries(e)) return;\n depth.current += 1;\n setOver(true);\n },\n onDragOver: (e: DragEvent) => {\n if (!carries(e)) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n },\n onDragLeave: (e: DragEvent) => {\n if (!carries(e)) return;\n depth.current = Math.max(0, depth.current - 1);\n if (depth.current === 0) setOver(false);\n },\n onDrop: (e: DragEvent) => {\n if (!carries(e)) return;\n e.preventDefault();\n depth.current = 0;\n setOver(false);\n const id = e.dataTransfer.getData(DRAG_TYPE);\n if (id) onDropConversation(id);\n },\n }\n : {})}\n >\n {children}\n </div>\n );\n};\n\n/** A one-line name field: Enter commits, Escape or an empty blur cancels. */\nconst NameField = ({\n initial = '',\n placeholder,\n onSubmit,\n onCancel,\n}: {\n initial?: string;\n placeholder: string;\n onSubmit: (name: string) => void;\n onCancel: () => void;\n}) => {\n const [value, setValue] = useState(initial);\n const settled = useRef(false);\n const inputRef = useRef<HTMLInputElement>(null);\n useEffect(() => {\n inputRef.current?.focus();\n inputRef.current?.select();\n }, []);\n const commit = () => {\n if (settled.current) return;\n settled.current = true;\n onSubmit(value.trim());\n };\n return (\n <input\n ref={inputRef}\n value={value}\n maxLength={200}\n onChange={(e) => setValue(e.target.value)}\n onClick={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n e.stopPropagation();\n if (e.key === 'Enter') commit();\n if (e.key === 'Escape') {\n settled.current = true;\n onCancel();\n }\n }}\n onBlur={commit}\n placeholder={placeholder}\n aria-label={placeholder}\n className=\"mx-2 mb-1 w-[calc(100%-1rem)] rounded-md bg-white dark:bg-white/10 px-2 py-1 text-[0.8125rem] text-gray-900 dark:text-white outline-hidden ring-1 ring-[var(--chat-accent)]\"\n />\n );\n};\n\nconst WorkspaceGroupSection = ({\n workspace,\n count,\n open,\n toggleable,\n pending,\n onToggle,\n onNewChat,\n onRename,\n onDelete,\n onDropConversation,\n t,\n children,\n}: {\n /** `null` for the conversations filed where the list does not reach. */\n workspace: ChatWorkspace | null;\n count: number;\n open: boolean;\n /** False while the group is held open (a search, the conversation on screen). */\n toggleable: boolean;\n /** A new conversation is about to be created in this workspace. */\n pending: boolean;\n onToggle: () => void;\n onNewChat: (workspaceId: string) => void;\n onRename: (id: string, name: string) => void;\n onDelete: (id: string) => void;\n onDropConversation: (conversationId: string) => void;\n t: (key: string) => string;\n children: ReactNode;\n}) => {\n const [renaming, setRenaming] = useState(false);\n const [confirmingDelete, setConfirmingDelete] = useState(false);\n const manageable = !!workspace?.canManage;\n const isMyDefault = !!workspace && workspace.isDefault && workspace.isOwn;\n\n return (\n <DropZone onDropConversation={onDropConversation} accepts={manageable} className=\"mb-0.5\">\n <div className=\"group/ws flex items-center gap-1 rounded-lg px-1.5 py-1 hover:bg-gray-100 dark:hover:bg-white/[0.06]\">\n {renaming && workspace ? (\n <NameField\n initial={workspace.name}\n placeholder={t('Workspace name')}\n onSubmit={(name) => {\n setRenaming(false);\n if (name && name !== workspace.name) onRename(workspace.id, name);\n }}\n onCancel={() => setRenaming(false)}\n />\n ) : (\n <button\n type=\"button\"\n onClick={onToggle}\n disabled={!toggleable}\n aria-expanded={open}\n className=\"flex min-w-0 flex-1 items-center gap-1.5 text-left disabled:cursor-default\"\n >\n <ChevronRightIcon size={12} className={`shrink-0 text-gray-400 dark:text-white/30 transition-transform ${open ? 'rotate-90' : ''}`} />\n <FolderIcon size={14} className=\"shrink-0 text-[var(--chat-accent)]\" />\n <span className=\"truncate text-[0.8125rem] font-medium text-gray-800 dark:text-white/80\">\n {workspace ? workspace.name : t('Other workspaces')}\n </span>\n {isMyDefault && (\n <span className=\"shrink-0 rounded bg-[var(--chat-accent)]/10 px-1 text-[0.6rem] text-[var(--chat-accent)]\">{t('default')}</span>\n )}\n <span className=\"ml-auto shrink-0 pr-1 text-[0.625rem] tabular-nums text-gray-400 dark:text-white/30\">{count}</span>\n </button>\n )}\n {workspace && manageable && !renaming && (\n <span className=\"flex shrink-0 items-center opacity-0 group-hover/ws:opacity-100 focus-within:opacity-100 transition-opacity\">\n {confirmingDelete ? (\n <>\n <span className=\"px-1 text-[0.65rem] text-red-600 dark:text-red-400\">{t('Delete?')}</span>\n <button\n type=\"button\"\n onClick={() => {\n setConfirmingDelete(false);\n onDelete(workspace.id);\n }}\n aria-label={t('Delete workspace')}\n className={`${iconButton} hover:text-red-500`}\n >\n <CheckIcon size={13} />\n </button>\n <button type=\"button\" onClick={() => setConfirmingDelete(false)} aria-label={t('Cancel')} className={iconButton}>\n <CloseIcon size={13} />\n </button>\n </>\n ) : (\n <>\n <Tooltip title={t('New conversation in this workspace')}>\n <button\n type=\"button\"\n onClick={() => onNewChat(workspace.id)}\n aria-label={t('New conversation in this workspace')}\n className={iconButton}\n >\n <EditIcon size={13} />\n </button>\n </Tooltip>\n <Tooltip title={t('Rename workspace')}>\n <button type=\"button\" onClick={() => setRenaming(true)} aria-label={t('Rename workspace')} className={iconButton}>\n <PencilIcon size={13} />\n </button>\n </Tooltip>\n {/* The default cannot be deleted: every person keeps one. */}\n {!workspace.isDefault && (\n <Tooltip title={t('Delete workspace')}>\n <button\n type=\"button\"\n onClick={() => setConfirmingDelete(true)}\n aria-label={t('Delete workspace')}\n className={`${iconButton} hover:text-red-500`}\n >\n <TrashIcon size={13} />\n </button>\n </Tooltip>\n )}\n </>\n )}\n </span>\n )}\n </div>\n {open && (\n <div className=\"space-y-0.5 pb-1 pl-3\">\n {pending && (\n <div className=\"flex items-center gap-2 rounded-lg bg-[var(--chat-accent)]/10 px-2.5 py-1.5 text-[0.75rem] text-[var(--chat-accent)]\">\n <EditIcon size={12} />\n {t('New conversation — send a message to start it')}\n </div>\n )}\n {children}\n </div>\n )}\n </DropZone>\n );\n};\n\nconst ConversationRow = ({\n conversation: c,\n isActive,\n isEditing,\n draftTitle,\n onDraftTitleChange,\n onCommitRename,\n onCancelRename,\n onStartRename,\n onSelect,\n onDelete,\n fileable,\n onMove,\n actions,\n t,\n}: {\n conversation: ChatConversationSummary;\n isActive: boolean;\n isEditing: boolean;\n draftTitle: string;\n onDraftTitleChange: (title: string) => void;\n onCommitRename: () => void;\n onCancelRename: () => void;\n onStartRename?: (id: string, current: string) => void;\n onSelect: (id: string) => void;\n onDelete: (id: string) => void;\n /** Workspaces the row can be moved into; omitted when there are no workspaces. */\n fileable?: ChatWorkspace[];\n onMove: (conversationId: string, workspaceId: string | null) => void;\n actions?: (conversation: ChatConversationSummary) => ReactNode;\n t: (key: string) => string;\n}) => {\n const [moveOpen, setMoveOpen] = useState(false);\n const moveAnchor = useRef<HTMLButtonElement>(null);\n const movable = !!fileable && !isEditing;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n aria-current={isActive}\n draggable={movable}\n onDragStart={\n movable\n ? (e) => {\n e.dataTransfer.setData(DRAG_TYPE, c.conversationId);\n e.dataTransfer.effectAllowed = 'move';\n }\n : undefined\n }\n onClick={() => onSelect(c.conversationId)}\n onKeyDown={(e) => {\n // Only the row's own keys: Enter or Space on the move, rename and\n // delete buttons inside it belongs to those buttons.\n if (e.target !== e.currentTarget) return;\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) => onDraftTitleChange(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') onCommitRename();\n if (e.key === 'Escape') onCancelRename();\n }}\n onBlur={onCommitRename}\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 {actions && (\n // The row selects on click and on Enter/Space: the host's controls keep theirs.\n <span className=\"flex\" onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>\n {actions(c)}\n </span>\n )}\n {movable && (\n <>\n <button\n ref={moveAnchor}\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n setMoveOpen((v) => !v);\n }}\n aria-label={t('Move to workspace')}\n title={t('Move to workspace')}\n aria-expanded={moveOpen}\n className=\"p-1 rounded-md text-gray-400 dark:text-white/30 hover:text-[var(--chat-accent)]\"\n >\n <FolderInputIcon size={13} />\n </button>\n <Dropdown open={moveOpen} onClose={() => setMoveOpen(false)} anchorRef={moveAnchor} placement=\"bottom-end\" width={220}>\n <div className=\"py-1\" onClick={(e) => e.stopPropagation()}>\n <p className=\"px-3 pb-1 pt-1.5 text-[0.625rem] font-medium uppercase tracking-wide text-gray-400 dark:text-white/40\">\n {t('Move to workspace')}\n </p>\n {fileable.map((w) => (\n <button\n key={w.id}\n type=\"button\"\n disabled={w.id === c.workspaceId}\n onClick={() => {\n setMoveOpen(false);\n onMove(c.conversationId, w.id);\n }}\n className=\"flex w-full items-center gap-2 px-3 py-1.5 text-left text-[0.8125rem] text-gray-700 dark:text-white/80 hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-40\"\n >\n <FolderIcon size={13} className=\"shrink-0 text-[var(--chat-accent)]\" />\n <span className=\"truncate\">{w.name}</span>\n {w.id === c.workspaceId && <CheckIcon size={12} className=\"ml-auto shrink-0\" />}\n </button>\n ))}\n <div className=\"my-1 h-px bg-gray-200 dark:bg-white/10\" />\n <button\n type=\"button\"\n disabled={!c.workspaceId}\n onClick={() => {\n setMoveOpen(false);\n onMove(c.conversationId, null);\n }}\n className=\"flex w-full items-center gap-2 px-3 py-1.5 text-left text-[0.8125rem] text-gray-700 dark:text-white/80 hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-40\"\n >\n <CloseIcon size={13} className=\"shrink-0\" />\n {t('Remove from workspace')}\n </button>\n </div>\n </Dropdown>\n </>\n )}\n {onStartRename && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onStartRename(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","import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport type { ChatArtifact, ChatArtifactKind, ChatAttachment, ChatPanelHandle, ChatPanelProps, ChatSendOutcome, Translate } from '../types';\nimport { answerMarkdownSources, artifactFileExtension, filenameFromContentDisposition, hexAlpha, markerKinds } from '../utils';\nimport { speakableText } from '../utils/speech';\nimport { optInRoute } from '../utils/routes';\nimport { createTranslate } from '../i18n';\nimport { feedbackBaseUrl } from '../utils/feedback';\nimport { conversationReferencesUrl } from '../utils/conversationRefs';\nimport { pendingWorkspaceStillFileable, returnRefreshDue } from '../utils/workspaces';\nimport { readRefusalDetail, requestHeadersKey } from '../utils/refusal';\nimport { parseArtifact, parseContextUsage, parseRestoredMessages, storedMessageId } from '../hooks/protocols/parseRestEvent';\nimport { mergeReadBack, useChat } from '../hooks/useChat';\nimport { askHost } from '../utils/askHost';\nimport { findAgent, useAgents } from '../hooks/useAgents';\nimport { useConversations } from '../hooks/useConversations';\nimport { useWorkspaces } from '../hooks/useWorkspaces';\nimport { useSidebarResize } from '../hooks/useSidebarResize';\nimport { useAwayCompletionNotice } from '../hooks/useAwayCompletionNotice';\nimport { useComposerExtras } from '../hooks/useComposerExtras';\nimport { useAgentSuggestions } from '../hooks/useAgentSuggestions';\nimport { useLatestCallback, useShallowStable } from '../hooks/useStableProps';\nimport { useVoiceCall } from '../hooks/useVoiceCall';\nimport { PanelRequestsProvider, usePanelRequests, type PanelRequests } from '../hooks/usePanelRequests';\nimport { watchSessionEnd } from '../utils/sessionEnd';\nimport { AlertTriangleIcon, DefaultLogoIcon } from './icons';\nimport { ChatHeader } from './ChatHeader';\nimport { ChatInput } from './ChatInput';\nimport { ChatMessages } from './ChatMessages';\nimport { statusLineText } from './ChatThinking';\nimport { ChatVoiceCall } from './ChatVoiceCall';\nimport { waitingGameUrl } from './ChatWaitingPanel';\nimport { ChatWelcome } from './ChatWelcome';\nimport { ConversationSidebar } from './ConversationSidebar';\n\nconst FLOATING_WIDTH = 380;\nconst FLOATING_HEIGHT = 560;\nconst SIDEBAR_GAP = 6;\n/** How long a message jumped to stays highlighted. */\nconst HIGHLIGHT_MS = 2000;\n/** What the restore route answers, as far as the panel reads it. */\ninterface RestoredHistory {\n conversation_id?: unknown;\n agent_name?: unknown;\n has_more?: unknown;\n messages?: unknown[];\n}\n// A backend down, or out of its gateway's reach: it says nothing of the conversation asked for.\nconst BACKEND_DOWN_STATUSES: ReadonlySet<number> = new Set([502, 503, 504]);\n// The credentials refused, not the conversation: kept, and read back once the host's credentials change.\nconst RESTORE_REFUSED_STATUSES: ReadonlySet<number> = new Set([401, 403]);\nconst RESTORE_RETRY_BASE_MS = 5000;\nconst RESTORE_RETRY_MAX_MS = 60000;\n\n/**\n * Fallback suggestions, translated where they are written so extraction sees\n * the keys — the host's own list and the backend's are translated too (they may\n * be keys), but only these belong to the package.\n */\nconst defaultSuggestions = (t: Translate) => [\n t('Help me create a new simulation scenario'),\n t('What are the latest attack patterns?'),\n t('How do I configure detection rules?'),\n t('Summarize my recent findings'),\n];\n\n/**\n * The panel, its requests reporting the end of the user's session\n * (`onSessionEnded`): once per set of `requestHeaders`, whichever request or\n * stream said it first.\n */\nexport const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(function ChatPanel(props, ref) {\n const onSessionEndedRef = useRef(props.onSessionEnded);\n onSessionEndedRef.current = props.onSessionEnded;\n const headersKeyRef = useRef('');\n headersKeyRef.current = requestHeadersKey(props.requestHeaders);\n const requests = useMemo<PanelRequests>(() => {\n let endedFor: string | null = null;\n const sessionEnded = (reason: string | undefined) => {\n if (!onSessionEndedRef.current || endedFor === headersKeyRef.current) return;\n endedFor = headersKeyRef.current;\n askHost(onSessionEndedRef.current, reason);\n };\n return { sessionEnded, fetch: watchSessionEnd(() => (onSessionEndedRef.current ? sessionEnded : undefined)) };\n }, []);\n return (\n <PanelRequestsProvider value={requests}>\n <ChatPanelBody {...props} ref={ref} />\n </PanelRequestsProvider>\n );\n});\n\nconst ChatPanelBody = forwardRef<ChatPanelHandle, ChatPanelProps>(function ChatPanelBody(\n {\n mode,\n onClose,\n onModeChange,\n topOffset = 0,\n apiBaseUrl,\n apiEndpoints: apiEndpointsProp,\n agentDashboardUrl,\n waitingGameUrl: waitingGameUrlProp,\n user,\n t: hostT,\n accentColor = '#7b5cff',\n logoIcon,\n promptSuggestions: promptSuggestionsProp,\n draftBorderColor,\n resizable = false,\n onWidthChange,\n onResizeStart,\n onResizeEnd,\n disableFileManagement = false,\n onRelativeLinkClick: onRelativeLinkClickProp,\n onDownloadError: onDownloadErrorProp,\n maxFileCount,\n maxTotalSize,\n requestHeaders: requestHeadersProp,\n pageContext: pageContextProp,\n pushContentSelector,\n backendType = 'rest',\n miniGameEnabled = true,\n onPlayWaitingGame: onPlayWaitingGameProp,\n notifyOnComplete = true,\n onTaskComplete: onTaskCompleteProp,\n onMessageFeedback: onMessageFeedbackProp,\n locale,\n disableImagePreviews = false,\n contextUsageEnabled = true,\n composerToolbar,\n storageKeyPrefix,\n conversationId: controlledConversationId,\n onConversationChange: onConversationChangeProp,\n initialAgent,\n onAgentChange: onAgentChangeProp,\n readOnly = false,\n readOnlyNotice,\n getRequestExtras: getRequestExtrasProp,\n onStreamEvent: onStreamEventProp,\n markerRenderers: markerRenderersProp,\n codeBlockRenderers: codeBlockRenderersProp,\n transformMessageText,\n onArtifactOpen: onArtifactOpenProp,\n headerActions,\n banner,\n composerHeader,\n transcriptFooter,\n welcome,\n renderMessageExtras,\n messageFooterActions,\n sidebarRowActions,\n sidebarHeaderActions,\n sidebarSections,\n statusLabel,\n waitingMessages: waitingMessagesProp,\n renderWaitingPanel,\n fontFamily,\n onReady: onReadyProp,\n onBeforeSend: onBeforeSendProp,\n onHeldSendSettled: onHeldSendSettledProp,\n onTurnRunningChange: onTurnRunningChangeProp,\n steerAttachments = false,\n holdSendsDuringTurn = false,\n agentMentions: agentMentionsEnabled = false,\n hideConversationList = false,\n },\n ref,\n) {\n // Hosts tend to build these inline, so each host render hands the panel new\n // objects and functions with the same content. Held stable here, so a host\n // render re-renders no memoized message row, re-runs none of the effects\n // keyed on them (agent list, prompts, quota, suggestions, conversation list)\n // and refetches none of the images fetched with the headers. `t` passes\n // through as is, since a new one means new translations, and so do\n // ReactNode props, which cannot be compared.\n const apiEndpoints = useShallowStable(apiEndpointsProp);\n const requestHeaders = useShallowStable(requestHeadersProp);\n const pageContext = useShallowStable(pageContextProp);\n const promptSuggestions = useShallowStable(promptSuggestionsProp);\n const onRelativeLinkClick = useLatestCallback(onRelativeLinkClickProp);\n const onDownloadError = useLatestCallback(onDownloadErrorProp);\n const onPlayWaitingGame = useLatestCallback(onPlayWaitingGameProp);\n const onTaskComplete = useLatestCallback(onTaskCompleteProp);\n const onMessageFeedback = useLatestCallback(onMessageFeedbackProp);\n const onConversationChange = useLatestCallback(onConversationChangeProp);\n const onAgentChange = useLatestCallback(onAgentChangeProp);\n const getRequestExtras = useLatestCallback(getRequestExtrasProp);\n const onBeforeSend = useLatestCallback(onBeforeSendProp);\n const onHeldSendSettled = useLatestCallback(onHeldSendSettledProp);\n const onStreamEvent = useLatestCallback(onStreamEventProp);\n const onTurnRunningChange = useLatestCallback(onTurnRunningChangeProp);\n // A click handler, not content: held stable so a host's inline one spares every row.\n const onArtifactOpen = useLatestCallback(onArtifactOpenProp);\n const rootRef = useRef<HTMLDivElement>(null);\n const { fetch: panelFetch, sessionEnded } = usePanelRequests();\n // The panel's own catalog in `locale`, under whatever the host translates itself.\n const t = useMemo(() => createTranslate(hostT, locale), [hostT, locale]);\n const waitingMessages = useShallowStable(waitingMessagesProp);\n // The host's marker cards by upper-cased name, the form the parser reads.\n const markerRenderers = useMemo(\n () => (markerRenderersProp ? Object.fromEntries(Object.entries(markerRenderersProp).map(([k, r]) => [k.toUpperCase(), r])) : undefined),\n [markerRenderersProp],\n );\n // The host's code block renderers by lower-cased fence language, the form the renderer looks up.\n const codeBlockRenderers = useShallowStable(\n useMemo(\n () => (codeBlockRenderersProp ? Object.fromEntries(Object.entries(codeBlockRenderersProp).map(([k, r]) => [k.toLowerCase(), r])) : undefined),\n [codeBlockRenderersProp],\n ),\n );\n const markerKindList = useShallowStable(useMemo(() => markerKinds(markerRenderers && Object.keys(markerRenderers)), [markerRenderers]));\n const thinking = useMemo(() => ({ statusLabel, waitingMessages, renderWaitingPanel }), [statusLabel, waitingMessages, renderWaitingPanel]);\n // Inline mode is laid out like fullscreen, inside the host's container.\n const isWide = mode === 'fullscreen' || mode === 'inline';\n\n const [modeMenuOpen, setModeMenuOpen] = useState(false);\n\n const {\n agents,\n agentsLoading,\n agentsError,\n agentsSettled,\n agentsDisabled,\n selectedAgent,\n agentMenuOpen,\n setAgentMenuOpen,\n handleSwitchAgent,\n reloadAgents,\n } = useAgents({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n storageKeyPrefix,\n initialAgent,\n onAgentChange: onAgentChangeProp ? onAgentChange : undefined,\n });\n\n // The conversation whose restore settled (read back, failed or nothing to\n // read): the skeleton and onReady wait for it.\n const [historySettledId, setHistorySettledId] = useState<string | null>(null);\n const restoringIdRef = useRef<string | null>(null);\n // The conversation whose history this panel actually holds: only its event\n // stream is followed, so a relayed turn never lands in an empty transcript.\n const [eventsReadyId, setEventsReadyId] = useState<string | null>(null);\n // The conversation the gate above was last checked for, kept in state so a\n // render React throws away takes its reset with it.\n const [eventsGateFor, setEventsGateFor] = useState<string | null | undefined>(undefined);\n // A read-back that came while a turn was on screen: put above that turn as\n // soon as it ends.\n const [restorePostponed, setRestorePostponed] = useState<{ conversationId: string; data: RestoredHistory } | null>(null);\n // A conversation the backend could not be reached for: read back once it can.\n const [restoreOwedId, setRestoreOwedId] = useState<string | null>(null);\n const restoreAttemptsRef = useRef(0);\n // A conversation whose read-back refused the credentials (401, 403): what the\n // backend said, and the headers it refused.\n const [restoreRefused, setRestoreRefused] = useState<{ conversationId: string; detail: string | null; headersKey: string } | null>(null);\n // Retitled elsewhere, or by the backend after the first answer.\n const refreshConversationsRef = useRef<() => Promise<unknown>>(async () => undefined);\n // Every frame of both streams goes to the voice call first, then to the\n // host: each on its own, so neither throwing keeps the frame from the other.\n const voiceFrameRef = useRef<(frame: Record<string, unknown>, relayed: boolean) => void>(() => undefined);\n const handleStreamFrame = useCallback(\n (frame: Record<string, unknown>, info: { relayed: boolean }) => {\n askHost(voiceFrameRef.current, frame, info.relayed);\n askHost(onStreamEvent, frame, info);\n },\n [onStreamEvent],\n );\n\n const {\n messages,\n inputValue,\n setInputValue,\n isLoading,\n turnRunning,\n sendsHeld,\n turnEnd,\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 reloadHistory,\n mergeTurnHistory,\n settleUnseenEnd,\n historyLoadedRef,\n conversationIdRef,\n handleFileAdd,\n handlePaste,\n handleSendMessage,\n skipWait,\n readPendingApprovals,\n heldMessage,\n notice,\n cancelHeldMessage,\n agentMentions,\n addAgentMention,\n handleNewChat,\n pendingWorkspaceId,\n clearPendingWorkspace,\n handleStopGenerating,\n conversationRefs,\n addConversationRef,\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 onRelativeLinkClick: onRelativeLinkClickProp ? onRelativeLinkClick : undefined,\n t,\n maxFileCount,\n maxTotalSize,\n storageKeyPrefix,\n controlledConversationId,\n onConversationIdChange: onConversationChangeProp ? onConversationChange : undefined,\n getRequestExtras: getRequestExtrasProp ? getRequestExtras : undefined,\n onStreamEvent: handleStreamFrame,\n onSessionEnded: sessionEnded,\n onConversationTitle: () => void refreshConversationsRef.current(),\n historySettledFor: eventsReadyId,\n steerAttachments,\n holdSends: holdSendsDuringTurn,\n agentMentionsEnabled,\n onBeforeSend: onBeforeSendProp ? onBeforeSend : undefined,\n onHeldSendSettled: onHeldSendSettledProp ? onHeldSendSettled : undefined,\n });\n // A conversation left and come back to waits for its history again before\n // its event stream is followed. Reset while rendering, so the hook never\n // opens the stream on the earlier visit's word.\n if (eventsGateFor !== conversationId) {\n setEventsGateFor(conversationId);\n if (eventsReadyId !== null) setEventsReadyId(null);\n }\n\n // A host that owns the conversation (a `?c=` URL) moves the panel with it.\n useEffect(() => {\n if (controlledConversationId === undefined || controlledConversationId === conversationIdRef.current) return;\n if (controlledConversationId) handleSwitchConversation(controlledConversationId);\n else handleNewChat();\n // Only a change of the host's id moves the panel; the panel's own changes\n // reach the host through `onConversationChange`.\n }, [controlledConversationId]);\n\n const { suggestions: agentSuggestions, loading: suggestionsLoading } = useAgentSuggestions({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n agentSlug: selectedAgent?.slug,\n });\n\n /**\n * Suggestions shown on the welcome screen, resolved to final text here rather\n * than in `ChatWelcome`: the backend's and the host's lists still go through\n * `t` (a host may well pass keys), while the package's own fallback is\n * translated at its definition so the keys stay extractable.\n */\n const suggestions = useMemo(\n () => (agentSuggestions ?? promptSuggestions)?.map((s) => t(s)) ?? defaultSuggestions(t),\n [agentSuggestions, promptSuggestions, t],\n );\n\n const { prompts, quota, refreshQuota } = useComposerExtras({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n });\n\n const {\n historyEnabled,\n conversations,\n conversationsLoading,\n refreshConversations,\n deleteConversation,\n renameConversation,\n moveConversation,\n clearConversations,\n } = useConversations({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n });\n refreshConversationsRef.current = refreshConversations;\n // Workspaces group the fullscreen list, when the host names their route.\n const { workspacesEnabled, workspaces, refreshWorkspaces, createWorkspace, renameWorkspace, deleteWorkspace } = useWorkspaces({\n apiBaseUrl,\n apiEndpoints,\n backendType,\n requestHeaders,\n });\n // A pending workspace deleted, archived or unshared elsewhere leaves the\n // refreshed list: the next conversation must not be created in it.\n useEffect(() => {\n if (workspacesEnabled && pendingWorkspaceId && !pendingWorkspaceStillFileable(pendingWorkspaceId, workspaces)) {\n clearPendingWorkspace(pendingWorkspaceId);\n }\n }, [workspacesEnabled, workspaces, pendingWorkspaceId, clearPendingWorkspace]);\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 // A host may give the thread the whole width (a rail, a task pane): the\n // header's history menu then stands in for the list.\n const showConversationSidebar = isWide && historyEnabled && !hideConversationList;\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 // The workspaces are also changed elsewhere (the XTM One web chat, another\n // tab): read on entry, after every write the sidebar makes (the hook\n // re-reads them), on each conversation switch, at the end of each turn and\n // on each return to the page - a group deleted there must not linger here.\n useEffect(() => {\n if (showConversationSidebar) void refreshWorkspaces();\n }, [showConversationSidebar, conversationId, refreshWorkspaces]);\n\n // Back to the page (from the XTM One tab, say): both lists may have moved.\n const lastReturnRefreshRef = useRef<number | null>(null);\n useEffect(() => {\n if (!showConversationSidebar) return;\n const onReturn = () => {\n if (document.visibilityState !== 'visible') return;\n const now = Date.now();\n if (!returnRefreshDue(now, lastReturnRefreshRef.current)) return;\n lastReturnRefreshRef.current = now;\n void refreshConversations();\n void refreshWorkspaces();\n };\n document.addEventListener('visibilitychange', onReturn);\n window.addEventListener('focus', onReturn);\n return () => {\n document.removeEventListener('visibilitychange', onReturn);\n window.removeEventListener('focus', onReturn);\n };\n }, [showConversationSidebar, refreshConversations, refreshWorkspaces]);\n\n // The host hears when a turn starts or stops running, once per change, with\n // how it ended, and that it stopped when the panel goes away in the middle\n // of one.\n const reportedRunningRef = useRef(false);\n useEffect(() => {\n if (reportedRunningRef.current === turnRunning) return;\n reportedRunningRef.current = turnRunning;\n if (turnRunning) askHost(onTurnRunningChange, true);\n else askHost(onTurnRunningChange, false, { ended: turnEnd ?? 'stopped' });\n }, [turnRunning, turnEnd, onTurnRunningChange]);\n useEffect(\n () => () => {\n if (!reportedRunningRef.current) return;\n reportedRunningRef.current = false;\n askHost(onTurnRunningChange, false, { ended: 'stopped' });\n },\n [onTurnRunningChange],\n );\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 && !turnRunning) {\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. A conversation\n // that could not be created in its workspace (deleted elsewhere) is\n // read back unfiled: the workspaces are re-read with it.\n if (showConversationSidebar) {\n void refreshConversations();\n void refreshWorkspaces();\n }\n }\n wasLoadingRef.current = turnRunning;\n }, [turnRunning, refreshQuota, showConversationSidebar, refreshConversations, refreshWorkspaces]);\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 // The `@` conversation menu of the composer, when the host names its route.\n const conversationReferencesSearchUrl = conversationReferencesUrl(apiBaseUrl, apiEndpoints, backendType);\n\n // A referenced conversation's chip opens it in the panel when it is one the\n // conversation list holds, so the list is read as soon as the thread shows\n // a reference - not only once the history menu has been opened - and again\n // for each conversation switched to, whose chips may name newer ones.\n const openableConversationIds = useMemo(() => new Set(conversations.map((c) => c.conversationId)), [conversations]);\n const onOpenConversation = useLatestCallback(historyEnabled ? handleSelectConversation : undefined);\n const threadReferencesConversations = messages.some((m) => (m.conversationRefs?.length ?? 0) > 0);\n useEffect(() => {\n if (threadReferencesConversations && historyEnabled) void refreshConversations();\n }, [threadReferencesConversations, historyEnabled, refreshConversations, conversationId]);\n\n // Older messages the backend holds beyond the restored ones (`messagesPage`).\n const messagesPageBase = optInRoute(apiBaseUrl, apiEndpoints, backendType, 'messagesPage');\n const [serverHasEarlier, setServerHasEarlier] = useState(false);\n const ttsUrl = optInRoute(apiBaseUrl, apiEndpoints, backendType, 'tts');\n const ttsRoute = useMemo(() => (ttsUrl ? { url: ttsUrl, requestHeaders } : undefined), [ttsUrl, requestHeaders]);\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n useEffect(() => setServerHasEarlier(false), [conversationId]);\n const loadEarlier = useCallback(async (): Promise<number> => {\n const convId = conversationIdRef.current;\n const oldest = messagesRef.current.find((m) => m.serverId)?.serverId;\n if (!messagesPageBase || !convId || !oldest) {\n setServerHasEarlier(false);\n return 0;\n }\n try {\n const res = await panelFetch(\n `${messagesPageBase}/${encodeURIComponent(convId)}/messages?before=${encodeURIComponent(oldest)}&limit=50&supports_artifacts=true`,\n {\n headers: { ...(requestHeaders ?? {}) },\n },\n );\n if (!res.ok || conversationIdRef.current !== convId) return 0;\n const data = await res.json();\n if (conversationIdRef.current !== convId) return 0;\n const known = new Set(messagesRef.current.map((m) => m.serverId).filter(Boolean));\n const earlier = parseRestoredMessages(data?.messages)\n .filter((m) => !m.serverId || !known.has(m.serverId))\n .map((m) => ({ ...m, id: storedMessageId(m.serverId) }));\n setMessages((prev) => [...earlier, ...prev]);\n setServerHasEarlier(data?.has_more === true && earlier.length > 0);\n return earlier.length;\n } catch {\n return 0;\n }\n }, [messagesPageBase, conversationIdRef, requestHeaders, setMessages]);\n\n /**\n * Put a conversation's history read back on screen. *merge*: a turn ran\n * meanwhile, and the history joins what is shown (see `mergeReadBack`).\n */\n const applyRestoredHistory = (convId: string, data: RestoredHistory, merge: boolean) => {\n setEventsReadyId(convId);\n setServerHasEarlier(!!messagesPageBase && data.has_more === true);\n if (!data.messages?.length) return;\n const restored = parseRestoredMessages(data.messages);\n if (merge) {\n setMessages((prev) => mergeReadBack(prev, restored));\n return;\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\n // An owed read-back is tried again with backoff, and at once when the\n // browser is back online or the page is shown again - never over a turn\n // on screen, which the history read back would replace.\n const isLoadingNowRef = useRef(isLoading);\n isLoadingNowRef.current = isLoading;\n useEffect(() => {\n restoreAttemptsRef.current = 0;\n }, [conversationId]);\n useEffect(() => {\n if (!restorePostponed || isLoading) return;\n setRestorePostponed(null);\n if (restorePostponed.conversationId === conversationId) applyRestoredHistory(restorePostponed.conversationId, restorePostponed.data, true);\n }, [restorePostponed, isLoading, conversationId]);\n useEffect(() => {\n if (!restoreOwedId || restoreOwedId !== conversationId || isLoading) return;\n const retry = () => {\n if (isLoadingNowRef.current) return;\n setRestoreOwedId(null);\n restoreAttemptsRef.current += 1;\n reloadHistory();\n };\n const delay = Math.min(RESTORE_RETRY_MAX_MS, RESTORE_RETRY_BASE_MS * 2 ** restoreAttemptsRef.current);\n const timer = window.setTimeout(retry, delay);\n const onVisible = () => {\n if (document.visibilityState === 'visible') retry();\n };\n window.addEventListener('online', retry);\n document.addEventListener('visibilitychange', onVisible);\n return () => {\n window.clearTimeout(timer);\n window.removeEventListener('online', retry);\n document.removeEventListener('visibilitychange', onVisible);\n };\n }, [restoreOwedId, conversationId, isLoading, reloadHistory]);\n\n // A read-back refused for its credentials is tried again once the host's\n // credentials change (a token refreshed), or when the user asks.\n const headersKey = requestHeadersKey(requestHeaders);\n const retryRefusedRestore = useCallback(() => {\n setRestoreRefused(null);\n reloadHistory();\n }, [reloadHistory]);\n useEffect(() => {\n if (!restoreRefused || restoreRefused.conversationId !== conversationId || isLoading) return;\n if (restoreRefused.headersKey !== headersKey) retryRefusedRestore();\n }, [restoreRefused, conversationId, isLoading, headersKey, retryRefusedRestore]);\n const refusedRestore = restoreRefused && restoreRefused.conversationId === conversationId ? restoreRefused : null;\n\n // A conversation being read back shows its shape, not the welcome screen.\n const restoreEnabled = backendType === 'rest' && !apiEndpoints?.singleEndpoint && apiEndpoints?.sessions !== null;\n const restoringHistory = restoreEnabled && !!conversationId && historySettledId !== conversationId;\n\n // Files dragged over the thread attach on drop, as picking them would.\n const canDropFiles =\n !readOnly && !disableFileManagement && backendType === 'rest' && !apiEndpoints?.singleEndpoint && apiEndpoints?.upload !== null;\n const [dropping, setDropping] = useState(false);\n const dragDepthRef = useRef(0);\n const carriesFiles = (e: React.DragEvent) => e.dataTransfer.types.includes('Files');\n const dropHandlers = canDropFiles\n ? {\n onDragEnter: (e: React.DragEvent) => {\n if (!carriesFiles(e)) return;\n dragDepthRef.current += 1;\n setDropping(true);\n },\n onDragOver: (e: React.DragEvent) => {\n if (!carriesFiles(e)) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'copy';\n },\n onDragLeave: (e: React.DragEvent) => {\n if (!carriesFiles(e)) return;\n dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);\n if (dragDepthRef.current === 0) setDropping(false);\n },\n onDrop: (e: React.DragEvent) => {\n if (!carriesFiles(e)) return;\n e.preventDefault();\n // The host's own drop zones under the panel do not take the files too.\n e.stopPropagation();\n dragDepthRef.current = 0;\n setDropping(false);\n handleFileAdd(e.dataTransfer.files);\n },\n }\n : {};\n\n const handleClearAll = async () => {\n if (await clearConversations?.()) handleNewChat();\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 storageKeyPrefix,\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 playUrl = useMemo(() => waitingGameUrl(agentDashboardUrl, waitingGameUrlProp), [agentDashboardUrl, waitingGameUrlProp]);\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: the backend routes\n * a reopened conversation, and its new replies, to its own stored agent\n * whichever agent is selected, so that is the agent its label names.\n */\n const [conversationAgentName, setConversationAgentName] = useState<string | null>(null);\n const agentName = transferredAgent?.name || conversationAgentName || selectedAgent?.name || t('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 const panel = rootRef.current 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: turnRunning,\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 panelFetch(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 // The decks, reports and videos an answer made are downloaded and followed\n // through the host's proxy too, once the host names the route\n // (`apiEndpoints.artifacts`, no default). Without it the cards still show\n // and link to XTM One.\n const artifactsBase = backendType === 'rest' && apiEndpoints?.artifacts ? `${apiBaseUrl}${apiEndpoints.artifacts}` : null;\n const artifactUrl = useCallback(\n (kind: ChatArtifactKind, id: string) => `${artifactsBase}/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`,\n [artifactsBase],\n );\n\n const handleDownloadArtifact = useCallback(\n async (artifact: ChatArtifact, format: string) => {\n const fallbackName = `${artifact.title || artifact.kind}.${artifactFileExtension(format)}`;\n try {\n const res = await panelFetch(`${artifactUrl(artifact.kind, artifact.id)}/download`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json', ...(requestHeaders ?? {}) },\n body: JSON.stringify({ format }),\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 = filenameFromContentDisposition(res.headers.get('content-disposition')) ?? fallbackName;\n document.body.appendChild(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(objectUrl);\n } catch (err) {\n // The host's notification path for a failed download, named after the\n // file the user asked for.\n onDownloadError?.(err, { fileId: artifact.id, filename: fallbackName, type: format.toUpperCase() });\n }\n },\n [artifactUrl, requestHeaders, onDownloadError],\n );\n\n const handleRefreshArtifact = useCallback(\n async (kind: ChatArtifactKind, id: string) => {\n const res = await panelFetch(artifactUrl(kind, id), { credentials: 'include', headers: { ...(requestHeaders ?? {}) } });\n if (!res.ok) return undefined;\n return parseArtifact(await res.json());\n },\n [artifactUrl, requestHeaders],\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 ...(fontFamily ? { fontFamily } : {}),\n } as React.CSSProperties;\n\n // The restore merges a replayed turn's read-back through this, outside its deps.\n const mergeTurnHistoryRef = useRef(mergeTurnHistory);\n mergeTurnHistoryRef.current = mergeTurnHistory;\n const settleUnseenEndRef = useRef(settleUnseenEnd);\n settleUnseenEndRef.current = settleUnseenEnd;\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 setHistorySettledId(null);\n return;\n }\n if (historyLoadedRef.current) {\n // Created by this panel, or already restored: nothing to wait for.\n if (restoringIdRef.current !== conversationId && historySettledId !== conversationId) setHistorySettledId(conversationId);\n // A history held back behind a turn opens the gate when it is put on screen.\n if (restoringIdRef.current !== conversationId && eventsReadyId !== conversationId && restorePostponed?.conversationId !== conversationId) {\n setEventsReadyId(conversationId);\n }\n return;\n }\n // The agent goes with the restore, so it waits for one - unless the host\n // serves no agent list at all. A list that failed or holds none restores\n // nothing, yet the panel is not left waiting: the id is kept for a later try.\n if (!selectedAgent && !agentsDisabled) {\n if (agentsSettled && historySettledId !== conversationId) setHistorySettledId(conversationId);\n return;\n }\n historyLoadedRef.current = true;\n restoringIdRef.current = conversationId;\n setHistorySettledId(null);\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 const requestedHeadersKey = requestHeadersKey(requestHeaders);\n // What a turn that ended out of sight is told of this read-back (see `settleUnseenEnd`).\n const readFor = historyReloadNonce;\n\n panelFetch(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 supports_artifacts: true,\n }),\n })\n .then((res) => {\n if (isStale()) return null;\n if (!res.ok) {\n settleUnseenEndRef.current(readFor, null);\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\". A backend that is\n // down or unreachable through its gateway says nothing of the id,\n // which is kept and read back once it answers.\n // Credentials refused (an expired token) say nothing of the id\n // either: it is kept, with what the backend said, until the host's\n // credentials change or the user retries.\n if (RESTORE_REFUSED_STATUSES.has(res.status)) {\n return readRefusalDetail(res).then((detail) => {\n if (!isStale()) setRestoreRefused({ conversationId: requestedConversationId, detail, headersKey: requestedHeadersKey });\n return null;\n });\n }\n if (BACKEND_DOWN_STATUSES.has(res.status)) setRestoreOwedId(requestedConversationId);\n else updateConversationId(null);\n return null;\n }\n restoreAttemptsRef.current = 0;\n setRestoreOwedId(null);\n setRestoreRefused(null);\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 const restored = parseRestoredMessages(data.messages);\n settleUnseenEndRef.current(readFor, restored);\n // Read back for a relayed turn the reopened event stream replayed: the\n // segments it closed meanwhile go into it, and it runs on.\n if (mergeTurnHistoryRef.current(restored)) return;\n // A turn started meanwhile is on screen: the history waits for it to\n // end, and so does the event stream, which follows that history.\n if (isLoadingNowRef.current) {\n setRestorePostponed({ conversationId: requestedConversationId, data });\n return;\n }\n applyRestoredHistory(requestedConversationId, data, false);\n })\n // Unreachable: nothing is known of the conversation, whose id is kept.\n .catch(() => {\n if (isStale()) return;\n settleUnseenEndRef.current(readFor, null);\n setRestoreOwedId(requestedConversationId);\n })\n .finally(() => {\n if (restoringIdRef.current === requestedConversationId) restoringIdRef.current = null;\n if (!isStale()) setHistorySettledId(requestedConversationId);\n });\n }, [\n conversationId,\n selectedAgent,\n agentsSettled,\n historySettledId,\n messagesPageBase,\n apiBaseUrl,\n apiEndpoints,\n backendType,\n restorePostponed,\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 // A send from the handle runs against the render it lands in, not the one\n // that built the handle.\n const handleSendMessageRef = useRef(handleSendMessage);\n handleSendMessageRef.current = handleSendMessage;\n const handleNewChatRef = useRef(handleNewChat);\n handleNewChatRef.current = handleNewChat;\n const handleSwitchConversationRef = useRef(handleSwitchConversation);\n handleSwitchConversationRef.current = handleSwitchConversation;\n const agentsRef = useRef(agents);\n agentsRef.current = agents;\n const selectedAgentRef = useRef(selectedAgent);\n selectedAgentRef.current = selectedAgent;\n\n // Ready once the agent is chosen and the conversation on screen is read back:\n // what the host does through the handle before then waits for it.\n const ready = agentsSettled && !restoringHistory;\n const onReady = useLatestCallback(onReadyProp);\n const readyAnnouncedRef = useRef(false);\n useEffect(() => {\n if (!ready || readyAnnouncedRef.current) return;\n readyAnnouncedRef.current = true;\n onReady?.();\n }, [ready, onReady]);\n // Handle calls run one per render, in order: a new conversation's agent is\n // in place before the send that follows it.\n // A call the panel never gets to (it unmounts first) is let go of with `drop`.\n const queueRef = useRef<{ run: () => void; drop?: () => void }[]>([]);\n const [queueTick, setQueueTick] = useState(0);\n const enqueue = (run: () => void, drop?: () => void) => {\n queueRef.current.push({ run, drop });\n setQueueTick((n) => n + 1);\n };\n useEffect(() => {\n if (!ready || queueRef.current.length === 0) return;\n const call = queueRef.current.shift()!;\n call.run();\n if (queueRef.current.length > 0) setQueueTick((n) => n + 1);\n }, [ready, queueTick]);\n // Let go of once the panel is gone for good: StrictMode's simulated\n // unmount is followed at once by the remount, which runs them.\n useEffect(\n () => () => {\n queueMicrotask(() => {\n if (isMountedRef.current) return;\n for (const call of queueRef.current.splice(0)) call.drop?.();\n });\n },\n [],\n );\n\n // The handle stays the same object for the panel's life: its calls read the latest state.\n const handleSwitchAgentRef = useRef(handleSwitchAgent);\n handleSwitchAgentRef.current = handleSwitchAgent;\n const handleStopGeneratingRef = useRef(handleStopGenerating);\n handleStopGeneratingRef.current = handleStopGenerating;\n const readOnlyRef = useRef(readOnly);\n readOnlyRef.current = readOnly;\n // The composer's text for `getDraft`: as last rendered, or as `setDraft` just set it.\n const draftRef = useRef(inputValue);\n draftRef.current = inputValue;\n\n // The voice call: questions go out through the panel's own send, and the\n // answer is read off its streams. A question addresses the conversation's\n // agent: the one it was transferred to, the restored conversation's, or the\n // one picked.\n const addressedAgentId =\n transferredAgent?.id ??\n (conversationAgentName ? agents.find((a) => a.name === conversationAgentName)?.id : undefined) ??\n selectedAgent?.id ??\n null;\n const voice = useVoiceCall({\n send: (text, hooks) =>\n handleSendMessageRef.current({ text, source: 'voice', onSent: hooks.onAccepted, onNotSent: hooks.onNotSent, onSteered: hooks.onSteered }),\n stopTurn: () => handleStopGeneratingRef.current(),\n // Whether the turn still runs, read where a paused turn is recovered from.\n readTurnState: readPendingApprovals\n ? async () => {\n const convId = conversationIdRef.current;\n const state = convId ? await readPendingApprovals(convId) : null;\n return state && { running: state.turnRunning };\n }\n : undefined,\n addressedAgentId: () => addressedAgentId,\n speakable: (text) => speakableText(answerMarkdownSources(text, markerKindList)),\n present: (text) =>\n transformMessageText ? transformMessageText(text, { id: 'voice-call', role: 'assistant', content: text, timestamp: new Date() }) : text,\n tts: ttsRoute,\n locale,\n });\n voiceFrameRef.current = voice.onFrame;\n\n const onSwitchAgent = (agent: typeof selectedAgent) => {\n if (!agent) return;\n handleSwitchAgent(agent, () => {\n handleNewChat();\n });\n };\n\n useImperativeHandle(\n ref,\n () => ({\n send: (text, options) =>\n readOnlyRef.current\n ? Promise.resolve('dropped' as const)\n : new Promise<ChatSendOutcome>((resolve) =>\n enqueue(\n () => {\n if (readOnlyRef.current) {\n resolve('dropped');\n return;\n }\n // A new turn resolves once the backend takes it, not when its answer ends.\n handleSendMessageRef\n .current({ text, extras: options?.extras, displayText: options?.displayText, onSent: () => resolve('sent') })\n .then(resolve, () => resolve('failed'));\n },\n () => resolve('dropped'),\n ),\n ),\n newConversation: (options) =>\n enqueue(() => {\n const agent = findAgent(agentsRef.current, options?.agent);\n if (agent && agent.id !== selectedAgentRef.current?.id) handleSwitchAgentRef.current(agent);\n handleNewChatRef.current(options?.workspaceId);\n }),\n selectConversation: (id) => enqueue(() => handleSwitchConversationRef.current(id)),\n setDraft: (text) => {\n draftRef.current = text;\n setInputValue(text);\n },\n getDraft: () => draftRef.current,\n focusComposer: () => rootRef.current?.querySelector<HTMLTextAreaElement>('textarea')?.focus(),\n stop: () => {\n // A question a voice call awaits goes with its turn.\n voice.cancel();\n handleStopGeneratingRef.current();\n },\n startVoiceCall: () => !readOnlyRef.current && voice.start(),\n reloadAgents,\n scrollToMessage: (messageId) => {\n const target = [...(rootRef.current?.querySelectorAll<HTMLElement>('[data-message-id]') ?? [])].find(\n (el) => el.dataset.messageId === messageId,\n );\n if (!target) return false;\n target.scrollIntoView?.({ block: 'center', behavior: 'smooth' });\n target.classList.add('filigran-chat-highlight');\n window.setTimeout(() => target.classList.remove('filigran-chat-highlight'), HIGHLIGHT_MS);\n return true;\n },\n }),\n // setInputValue, reloadAgents and the voice call's controls are stable; everything else goes through a ref.\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 case 'inline':\n return `${base} relative flex flex-col h-full w-full min-h-0 bg-gray-50 dark:bg-[#161622]`;\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 : mode === 'inline'\n ? {}\n : { top: topOffset }),\n };\n\n return (\n <div ref={rootRef} className={containerClasses} style={containerStyle} data-testid=\"filigran-chat-panel\">\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 actions={headerActions}\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 onClearAll={clearConversations ? () => void handleClearAll() : undefined}\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 workspaces={\n workspacesEnabled\n ? {\n list: workspaces,\n pendingWorkspaceId,\n onCreate: createWorkspace,\n onRename: renameWorkspace,\n onDelete: async (id) => {\n const result = await deleteWorkspace(id);\n if (result.ok) {\n // The next conversation must not be created in it.\n clearPendingWorkspace(id);\n // Its conversations are back in no workspace (`workspace_id: null`).\n void refreshConversations();\n }\n return result;\n },\n onMove: moveConversation,\n onNewChat: (workspaceId) => {\n handleNewChat(workspaceId);\n // Still there? Deleted elsewhere, the list drops it and\n // the pending workspace with it, before the first send.\n void refreshWorkspaces();\n },\n }\n : undefined\n }\n storageKeyPrefix={storageKeyPrefix}\n rowActions={sidebarRowActions}\n headerActions={sidebarHeaderActions}\n sections={sidebarSections}\n onClearAll={clearConversations ? () => void handleClearAll() : undefined}\n t={t}\n />\n )}\n <div className={showConversationSidebar ? 'relative flex flex-1 flex-col min-w-0' : 'contents'} {...dropHandlers}>\n {dropping && (\n <div className=\"pointer-events-none absolute inset-2 z-30 flex items-center justify-center rounded-xl border-2 border-dashed border-[var(--chat-accent)] bg-[var(--chat-accent)]/10 text-sm font-medium text-[var(--chat-accent)]\">\n {t('Drop files to attach them')}\n </div>\n )}\n {banner && <div className=\"shrink-0\">{banner}</div>}\n {messages.length === 0 && restoringHistory ? (\n <div data-testid=\"filigran-chat-loading\" className=\"flex-1 flex flex-col gap-4 px-4 py-6\" aria-busy=\"true\">\n {[0, 1, 2].map((i) => (\n <div key={i} className={`flex ${i % 2 ? 'justify-end' : 'justify-start'}`}>\n <div className={`h-10 rounded-xl bg-gray-200/70 dark:bg-white/[0.06] animate-pulse ${i % 2 ? 'w-1/3' : 'w-2/3'}`} />\n </div>\n ))}\n </div>\n ) : messages.length === 0 && refusedRestore ? (\n <div\n data-testid=\"filigran-chat-restore-error\"\n role=\"alert\"\n className=\"flex-1 flex flex-col items-center justify-center gap-3 px-6 py-6 text-center\"\n >\n <AlertTriangleIcon size={18} className=\"text-amber-500 dark:text-amber-400\" />\n <p className=\"max-w-sm text-[0.8125rem] leading-6 text-gray-600 dark:text-white/60\">\n {refusedRestore.detail ?? t('This conversation could not be loaded.')}\n </p>\n <button\n type=\"button\"\n onClick={retryRefusedRestore}\n className=\"rounded-md border border-gray-200 dark:border-white/10 px-3 py-1 text-[0.75rem] font-medium text-gray-700 dark:text-white/80 transition-colors hover:bg-gray-100 dark:hover:bg-white/[0.06]\"\n >\n {t('Retry')}\n </button>\n </div>\n ) : messages.length === 0 ? (\n welcome ? (\n <div className=\"flex-1 min-h-0 overflow-y-auto filigran-chat-scrollable\">{welcome}</div>\n ) : (\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, this package's list when there is neither —\n // never an empty section.\n promptSuggestions={suggestions}\n suggestionsLoading={suggestionsLoading}\n agentName={selectedAgent?.name}\n agentDescription={selectedAgent?.description}\n onPromptClick={readOnly ? undefined : setInputValue}\n t={t}\n />\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 onDownloadArtifact={artifactsBase ? handleDownloadArtifact : undefined}\n onRefreshArtifact={artifactsBase ? handleRefreshArtifact : undefined}\n requestHeaders={requestHeaders}\n miniGameEnabled={miniGameEnabled}\n onPlayWaitingGame={onPlayWaitingGame}\n waitingGameUrl={playUrl}\n onMessageFeedback={onMessageFeedback}\n feedbackUrl={feedbackBaseUrl(apiBaseUrl, apiEndpoints, backendType)}\n conversationId={conversationId}\n locale={locale}\n isResumingAfterDecision={isResumingAfterDecision}\n onSkipWait={skipWait ?? undefined}\n pendingApprovals={pendingApprovals}\n onSubmitApprovalDecisions={submitApprovalDecisions}\n isSubmittingApproval={isSubmittingApproval}\n approvalError={approvalError}\n openableConversationIds={openableConversationIds}\n onOpenConversation={onOpenConversation}\n markerRenderers={markerRenderers}\n markerKindList={markerKindList}\n codeBlockRenderers={codeBlockRenderers}\n transformMessageText={transformMessageText}\n onArtifactOpen={onArtifactOpen}\n renderMessageExtras={renderMessageExtras}\n messageFooterActions={messageFooterActions}\n transcriptFooter={transcriptFooter}\n onLoadEarlier={serverHasEarlier ? loadEarlier : undefined}\n ttsRoute={ttsRoute}\n thinking={thinking}\n t={t}\n />\n )}\n {readOnly ? (\n readOnlyNotice && (\n <div className=\"px-4 py-3 border-t border-gray-200 dark:border-white/10 text-sm text-gray-500 dark:text-white/50\">{readOnlyNotice}</div>\n )\n ) : (\n <ChatInput\n header={composerHeader}\n sendsHeld={sendsHeld}\n steerAttachments={steerAttachments}\n canHold={holdSendsDuringTurn}\n heldMessage={heldMessage}\n notice={notice}\n onCancelHeld={cancelHeldMessage}\n agentMentions={agentMentionsEnabled && agents.length > 0 ? { agents, mentions: agentMentions, onPick: addAgentMention } : undefined}\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 conversationReferences={\n conversationReferencesSearchUrl\n ? { url: conversationReferencesSearchUrl, requestHeaders, conversationId, refs: conversationRefs, onPick: addConversationRef }\n : undefined\n }\n />\n )}\n </div>\n </div>\n {voice.snapshot && (\n <ChatVoiceCall\n call={voice.snapshot}\n agentName={agentName}\n logoIcon={resolvedLogo}\n processingLabel={statusLineText(agentStatus, t, statusLabel)}\n onInterrupt={voice.interrupt}\n onToggleMute={voice.toggleMute}\n onEnd={voice.end}\n t={t}\n />\n )}\n </div>\n );\n});\n","/**\n * How the panel tells that the user's session ended: a `401` whose body's\n * `detail.code` is `session_expired` (a stream says it with a\n * `session_expired` frame, read by the protocol parser).\n */\n\nconst SESSION_EXPIRED = 'session_expired';\n\n/** The session's end a refusal body states, with its `reason`; null for any other body. */\nexport function refusalSessionEnd(body: unknown): { reason: string | undefined } | null {\n if (!body || typeof body !== 'object') return null;\n const detail = (body as Record<string, unknown>).detail;\n if (!detail || typeof detail !== 'object') return null;\n const { code, reason } = detail as Record<string, unknown>;\n if (code !== SESSION_EXPIRED) return null;\n return { reason: typeof reason === 'string' && reason ? reason : undefined };\n}\n\n/**\n * `fetch`, reporting each `401` whose body says the session ended to what\n * *reporter* returns (when it returns nothing, no body is read). The body is\n * read from a clone, so the caller reads the response as it would have.\n */\nexport function watchSessionEnd(reporter: () => ((reason: string | undefined) => void) | undefined): typeof fetch {\n return async (input, init) => {\n // Looked up per call: a test, or a host's polyfill, may replace it later.\n const res = await globalThis.fetch(input, init);\n const report = res.status === 401 ? reporter() : undefined;\n if (report && typeof res.clone === 'function') {\n const copy = res.clone();\n void copy\n .json()\n .then((body: unknown) => {\n const end = refusalSessionEnd(body);\n if (end) report(end.reason);\n })\n .catch(() => undefined);\n }\n return res;\n };\n}\n","import { useCallback, useRef, useState } from 'react';\nimport type { ApiEndpoints, BackendType, ChatWorkspace } from '../types';\nimport { readRefusalDetail } from '../utils/refusal';\nimport { isTransientListFailure, parseWorkspaces } from '../utils/workspaces';\nimport { usePanelFetch } from './usePanelRequests';\n\ninterface UseWorkspacesOptions {\n apiBaseUrl: string;\n apiEndpoints?: ApiEndpoints;\n backendType?: BackendType;\n requestHeaders?: Record<string, string>;\n}\n\n/** What a workspace write answered: done, or the backend's reason. */\nexport type WorkspaceWriteResult = { ok: true; workspace?: ChatWorkspace } | { ok: false; error: string | null };\n\ninterface UseWorkspacesReturn {\n /** The host named the route (`apiEndpoints.workspaces`) and the backend answered the list. */\n workspacesEnabled: boolean;\n workspaces: ChatWorkspace[];\n refreshWorkspaces: () => Promise<void>;\n createWorkspace: (name: string) => Promise<WorkspaceWriteResult>;\n renameWorkspace: (id: string, name: string) => Promise<WorkspaceWriteResult>;\n deleteWorkspace: (id: string) => Promise<WorkspaceWriteResult>;\n}\n\n/**\n * The caller's workspaces, which the conversation list is grouped by.\n *\n * Off unless the host names `apiEndpoints.workspaces` (see its doc: no\n * default). A backend that refuses the list - not licensed, an older proxy -\n * also leaves it off, so the history stays the flat list it always was.\n */\nexport function useWorkspaces({ apiBaseUrl, apiEndpoints, backendType = 'rest', requestHeaders }: UseWorkspacesOptions): UseWorkspacesReturn {\n const panelFetch = usePanelFetch();\n const [workspaces, setWorkspaces] = useState<ChatWorkspace[]>([]);\n const [answered, setAnswered] = useState(false);\n // Refreshes overlap (after each write, on remount): only the latest answers.\n const latestRef = useRef(0);\n\n const path = apiEndpoints?.workspaces;\n const configured = backendType === 'rest' && !apiEndpoints?.singleEndpoint && typeof path === 'string' && path.length > 0;\n const url = configured ? `${apiBaseUrl}${path}` : null;\n\n const refreshWorkspaces = useCallback(async () => {\n if (!url) return;\n const ticket = ++latestRef.current;\n try {\n const res = await panelFetch(url, { method: 'GET', headers: { ...(requestHeaders ?? {}) } });\n if (ticket !== latestRef.current) return;\n if (!res.ok) {\n // A passing failure keeps the last list: emptying it would turn the\n // grouped history flat until remount. Only a refusal turns it off.\n if (!isTransientListFailure(res.status)) {\n setAnswered(false);\n setWorkspaces([]);\n }\n return;\n }\n const list = parseWorkspaces(await res.json());\n if (ticket !== latestRef.current) return;\n setWorkspaces(list);\n setAnswered(true);\n } catch {\n // A network failure or an unreadable body: keep the last list.\n }\n }, [url, requestHeaders]);\n\n const write = useCallback(\n async (method: 'POST' | 'PATCH' | 'DELETE', target: string, body?: Record<string, unknown>): Promise<WorkspaceWriteResult> => {\n try {\n const res = await panelFetch(target, {\n method,\n headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), ...(requestHeaders ?? {}) },\n ...(body ? { body: JSON.stringify(body) } : {}),\n });\n if (!res.ok) return { ok: false, error: await readRefusalDetail(res) };\n const created = res.status === 204 ? [] : parseWorkspaces([await res.json().catch(() => null)]);\n await refreshWorkspaces();\n return { ok: true, workspace: created[0] };\n } catch {\n return { ok: false, error: null };\n }\n },\n [requestHeaders, refreshWorkspaces],\n );\n\n const createWorkspace = useCallback(\n async (name: string) => (url ? write('POST', url, { name: name.trim() }) : { ok: false as const, error: null }),\n [url, write],\n );\n const renameWorkspace = useCallback(\n async (id: string, name: string) =>\n url ? write('PATCH', `${url}/${encodeURIComponent(id)}`, { name: name.trim() }) : { ok: false as const, error: null },\n [url, write],\n );\n const deleteWorkspace = useCallback(\n async (id: string) => (url ? write('DELETE', `${url}/${encodeURIComponent(id)}`) : { ok: false as const, error: null }),\n [url, write],\n );\n\n return {\n workspacesEnabled: configured && answered,\n workspaces,\n refreshWorkspaces,\n createWorkspace,\n renameWorkspace,\n deleteWorkspace,\n };\n}\n","import type { FunctionComponent } from 'react';\nimport type { ChatToggleButtonProps } from '../types';\nimport { useMemo } from 'react';\nimport { hexAlpha } from '../utils';\nimport { createTranslate } from '../i18n';\nimport { DefaultLogoIcon } from './icons';\n\nexport const ChatToggleButton: FunctionComponent<ChatToggleButtonProps> = ({\n isOpen,\n onToggle,\n label,\n accentColor = '#7b5cff',\n icon,\n t: hostT,\n locale,\n}) => {\n const t = useMemo(() => createTranslate(hostT, locale), [hostT, locale]);\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 ?? t('Ask Assistant')}\n </button>\n );\n};\n"],"names":["hexAlpha","hex","alpha","Math","round","toString","padStart","prepareMarkdown","content","raw","lines","split","fenceRe","markupLang","openerIdx","i","length","m","match","test","trim","fences","push","idx","run","bare","blankBetween","a","b","k","depth","maxRun","nestedCount","closerIdx","j","f","next","max","fence","repeat","om","cm","join","hardenNestedCodeFences","indexOf","splitCells","row","s","startsWith","slice","endsWith","cells","current","ch","isDelimiterRow","includes","alignOf","cell","c","left","right","fenceChar","fenceLen","listMarkerRe","fenceMatch","header","delim","markerMatch","offset","trimStart","indent","headerCols","delimCells","aligns","normalizeMarkdownTables","trimmed","JSON","parse","wrapBareJson","replace","_match","alt","url","SAFE_URL_PROTOCOLS","Set","markdownUrlTransform","colon","slash","question","hash","protocol","toLowerCase","has","identity","key","translate","t","values","slot","name","Object","hasOwn","String","findChatbotRoot","el","node","classList","contains","parentElement","document","body","compactCount","n","toFixed","timeAgo","iso","then","Date","getTime","Number","isNaN","diffMs","now","minutes","floor","count","hours","days","toLocaleDateString","undefined","month","day","BUILT_IN_MARKERS","BUILT_IN_MARKER_SOURCE","MARKER_NAME","escapeRe","text","grammars","Map","markerGrammar","hostKinds","extra","filter","cached","get","names","map","hostSource","heads","grammar","complete","RegExp","partial","longestHead","head","size","delete","keys","value","set","splitAnswerMarkers","streaming","parts","lastIndex","matchAll","index","type","kind","toUpperCase","args","fileId","id","slideRef","rest","tail","some","withoutMarkerHead","answerMarkdownSources","sources","part","artifactKey","artifactFacts","artifact","slideCount","sectionCount","sceneCount","duration","ms","total","formatVideoDuration","durationMs","groupAgents","agents","groups","agent","group","ungrouped","list","parser","unified","use","remarkParse","remarkGfm","freeze","SILENT","inlineText","children","collectLines","Boolean","child","speakableText","documents","markdown","line","FENCED_BLOCK_RE","chunkSpeech","maxLength","sentences","pieces","sentence","word","start","chunks","piece","last","async","speechAhead","fetchImpl","route","signal","request","res","method","headers","requestHeaders","stringify","format","ok","Error","status","blob","fetchSpeech","catch","aborted","optInRoute","apiBaseUrl","apiEndpoints","backendType","path","singleEndpoint","CATALOGS","es","Approved","Assistant","Back","Cancel","Close","Confirm","Conversation","Copied","Declined","Download","Floating","Input","Muted","No","Open","Output","PowerPoint","Presentation","Quota","Report","Retry","Save","Send","Sidebar","Skip","Stopped","Suggestions","Usage","Video","Workspaces","Yes","default","today","fr","ja","createTranslate","hostT","locale","catalog","tag","navigator","language","catalogFor","hosted","ratingOf","feedbackModeOf","serverId","feedbackUrl","conversationId","hasCallback","normalizeFeedbackComment","comment","feedbackBaseUrl","submitFeedback","baseUrl","messageId","rating","init","encodeURIComponent","credentials","feedbackRequest","isKeyChar","KEY_RE","referenceTokens","tokens","re","exec","pruneConversationRefs","refs","kept","r","referencedConversations","out","token","ref","parseReferenceCandidates","data","Array","isArray","conversations","o","title","updatedAt","updated_at","isOwn","is_own","conversationReferencesUrl","EMPTY_REFERENCE_SEARCH","query","items","failed","referenceMatches","candidate","q","words","every","w","rememberCandidates","seen","item","sameCandidate","ids","refAsCandidate","parseWorkspaces","options","workspaces","is_archived","userId","user_id","isDefault","is_default","canManage","can_manage","sortWorkspaces","mine","sort","localeCompare","groupIsOpen","collapsed","activeConversationId","searching","groupCanToggle","usable","readRefusalDetail","detail","inner","message","refusalDetail","json","requestHeadersKey","entries","randomId","crypto","randomUUID","bytes","getRandomValues","Uint8Array","from","BREAKDOWN_KEYS","parseContextUsage","evt","used","context_tokens","limit","context_window","isFinite","breakdown","src","any","wireKey","field","parseBreakdown","context_breakdown","parseAttachments","file_id","filename","contentType","content_type","fileTag","file_tag","ARTIFACT_KINDS","countOf","parseArtifact","find","slide_ref","slide_count","section_count","scene_count","duration_ms","progress","downloads","parseArtifacts","parseToolCallTrace","e","input","output","success","parseConversationRefs","conversation_id","parseTransferChain","agent_name","agentId","agent_id","agentName","parseToolApprovalProposals","p","toolCallId","tool_call_id","arguments","schema","input_schema","toolName","tool_name","toolDescription","tool_description","inputSchema","source","optionalString","v","parseAnsweredBy","by","agentIcon","agent_icon","fromEntries","parseRestEvent","ctx","message_id","provisional","end","action","reason","turnId","turn_id","parseRestoredMessages","role","storedMessageId","resumed","elapsedS","elapsed_s","st","thinkingContent","hasUsedTools","tools","event","action_id","pageAction","requested","str","parsePageAction","actionId","waitSeconds","seconds","waitReason","contextUsage","proposals","toolNames","tool_names","toolCallCount","tool_call_count","iterations","transferAgentId","transfer_agent_id","transferAgentName","transfer_agent_name","attachments","artifacts","reasoning","toolCallTrace","tool_call_trace","transferChain","transfer_chain","isTruncated","is_truncated","intermediate","answeredBy","answered_by","mentionFailed","mention_failed","unsaved","parsePersistedFeedback","forEach","createdAt","created_at","sentAt","timestamp","timestampUnknown","feedback","conversationRefs","conversation_refs","parseLegacyEvent","eventType","nodeId","activeNodeId","usedTools","tool","chatId","parseAgUiEvent","stepName","delta","toolCallName","createSessionGate","generation","pending","controller","started","own","AbortController","promise","finally","abandon","abort","hostTakesPageActions","approveUrl","onNavigate","baseHref","doc","defaultView","location","href","window","toInternalHref","base","URL","origin","pathname","search","insidePanel","closest","SECRET_AUTOCOMPLETE","hover","view","ownerDocument","bubbles","cancelable","pointerType","Pointer","PointerEvent","MouseEvent","Mouse","dispatchEvent","withoutDefault","types","act","suppress","preventDefault","addEventListener","removeEventListener","clickOrRoute","matches","target","getAttribute","error","internal","focus","anchor","inAnchor","cancels","checkable","before","checked","click","Event","visible","details","summary","tagName","hidden","hasAttribute","style","getComputedStyle","display","visibility","menuShowing","expanded","menu","getElementById","openMenu","statesItself","Promise","resolve","tick","setTimeout","Observer","MutationObserver","globalThis","observer","records","addedNodes","disconnect","clearTimeout","timer","observe","documentElement","subtree","childList","addsContent","SEARCH_WORDS","performPageAction","isConnected","fieldset","legend","isDisabled","isTypeable","readOnly","isReadOnly","isSecretField","HTMLInputElement","HTMLTextAreaElement","proto","prototype","getOwnPropertyDescriptor","call","textContent","setValue","attr","form","querySelectorAll","entersSearch","Keyboard","KeyboardEvent","code","keyCode","which","down","defaultPrevented","requestSubmit","pressEnter","err","TRUNCATION_MARKER","EXPLICIT_ROLES","wrapsControls","actionableRole","explicit","headingLevel","spacedText","childNodes","nodeType","isSecret","clean","NAMED_BY_CONTENT","accessibleName","labelledBy","fromAttr","label","getElementsByTagName","l","htmlFor","querySelector","contents","placeholder","quote","states","disabledByFieldset","destination","loc","STORAGE_KEYS","storageKey","prefix","area","localStorage","sessionStorage","readStorage","getItem","writeStorage","store","setItem","removeItem","readSseEvents","onEvent","reader","getReader","decoder","TextDecoder","buffer","handle","rawLine","done","read","decode","stream","pop","cancel","releaseLock","askHost","callback","console","hasFailedUpload","files","uploadStatus","agentMentionKey","slug","mentionedAgentIds","mentions","PanelRequestsContext","createContext","fetch","sessionEnded","PanelRequestsProvider","Provider","usePanelFetch","useContext","draftKey","draftRefsKey","loadDraftRefs","parsed","persistDraft","loadDraftState","STATUSES_DURING_WAIT","TURN_ACTIONS","PANEL_FIELDS","withDone","completedAt","STORED_ANSWER_FIELDS","SENT_TEXT","Symbol","withSentText","sentTextOf","shownKey","storedKey","adoptStored","shown","stored","fields","joinTexts","joinShown","displayText","joinHeldParts","refIds","mentionIds","draft","extras","NO_HELD_PART","heldMessageOf","reduce","mergeHeld","prev","tellNotSent","onNotSent","applySendDecision","decision","displayFallback","adjust","sent","fallback","turnEndOf","isProvisionalEnd","readRequestExtras","getter","DEFAULT_MAX_TOTAL_SIZE","lastStoredId","messages","useChat","agentSlug","pageContext","onRelativeLinkClick","maxFileCount","maxTotalSize","storageKeyPrefix","controlledConversationId","onConversationIdChange","getRequestExtras","onStreamEvent","onSessionEnded","onConversationTitle","historySettledFor","steerAttachments","holdSends","agentMentionsEnabled","onBeforeSend","onHeldSendSettled","panelFetch","isLegacy","setMessages","useState","isLoading","setIsLoading","turnEnd","setTurnEnd","agentStatus","setAgentStatus","isControlled","conversationStorageKey","legacyChatIdStorageKey","setConversationId","initialDraft","inputValue","setInputValue","setConversationRefs","agentMentions","setAgentMentions","attachedFiles","setAttachedFiles","transferredAgent","setTransferredAgent","setContextUsage","pendingApprovals","setPendingApprovals","isSubmittingApproval","setIsSubmittingApproval","approvalError","setApprovalError","isResumingAfterDecision","setIsResumingAfterDecision","historyReloadNonce","setHistoryReloadNonce","resumeTick","setResumeTick","legacyChatId","setLegacyChatId","historyLoadedRef","useRef","abortControllerRef","pageActionControllerRef","streamDeltasRef","ownTurnActiveRef","ownTurnIdRef","relayedTurnRef","cancelTimerRef","liveRendererRef","stoppedTurnIdRef","stopRequestedRef","heldMessage","setHeldMessage","heldMessageRef","notice","setNotice","noticeUntilSendRef","provisionalEndsRef","windDownRef","windingDown","setWindingDown","storedEndControllerRef","ownTurnStartedRef","cancelBeforeStartRef","turnFrameSeqRef","stopContextRef","resumeWaitRef","turnReadBackRef","withdrawnTurnRef","turnWithdrawn","setTurnWithdrawn","setWithdrawnTurn","turn","readBack","messagesRef","historyReloadsRef","clientId","setClientId","handoverTimerRef","handoverTurnIdRef","clearHandover","startWindDown","relayed","endWindDown","endRelayedWindDown","getRequestExtrasRef","onStreamEventRef","onSessionEndedRef","onConversationTitleRef","onBeforeSendRef","onHeldSendSettledRef","conversationIdRef","isLoadingRef","stopRunning","ended","pageContextRef","pageRefsRef","onRelativeLinkClickRef","requestHeadersRef","approvalConversationIdRef","approvalDetachedRef","probedConversationRef","resumeDeadlineRef","sessionGate","pendingWorkspaceRef","pendingWorkspaceId","setPendingWorkspaceId","uploadAbortRef","effectiveMaxFileCount","effectiveMaxTotalSize","useEffect","pruneAgentMentions","addAgentMention","useCallback","mention","withAgentMention","pick","addConversationRef","withConversationRef","returnToComposer","added","getSteerUrl","steer","getApproveUrl","getApproveUrlRef","readPendingApprovals","convId","getPendingApprovalsUrl","turnRunning","reloadHistory","getUploadUrl","upload","getCancelUrl","getSkipWaitUrl","eventsBase","hasEvents","pendingApprovalsBase","clientIdHeader","postConversation","getSessionsUrl","sessions","onConversationIdChangeRef","updateConversationId","notify","changed","ensureConversation","sessionsUrl","isCurrent","workspaceId","agent_slug","workspace_id","uploadSingleFile","file","uploadUrl","formData","FormData","append","uploadHeaders","file_ids","handleFileAdd","fileList","candidates","tempId","accepted","currentCount","currentSize","sum","slotsAvailable","sizeLeft","filtered","newEntries","rawFile","DOMException","answerPageAction","conversationIdHint","refuse","page_action","snapshot","result","quietMs","maxMs","quiet","cap","attributes","characterData","waitForDomSettle","page","snapshotError","maxChars","budget","truncated","walk","level","takePageSnapshot","createTurnRenderer","firstAssistantId","currentAssistantId","segmentIds","accumulated","doneReceived","answered","deltas","apply","textChanged","thinking","frame","closed","cancelFrame","cancelAnimationFrame","flush","chunk","schedule","requestAnimationFrame","close","createStreamDeltas","segId","ensureSegment","segmentId","elapsedStartMs","waitStartMs","applyStatus","applyDoneEffects","finalContent","landAnswer","applyDone","stopped","outcome","bubbleId","land","applyStoredEnd","readFrame","info","replayed","streamEventInfo","applyConversationFrame","relayedMessage","settleTurn","clearCancelRequest","handleSendMessage","override","fromComposer","typed","windingDownNow","asked","decided","hookExtras","sendExtras","shownText","heldDisplay","sentRefs","sentMentions","mentionedIds","clearComposer","uploaded","steered","steerUrl","optimistic","fileIds","unanswered","steerMessage","onSent","onSteered","userMsg","assistantId","turnGeneration","handedOver","sawTurnEnd","awaitingStoredEnd","released","windDown","handOver","pageActions","release","turnsOutliveStream","created","requestBody","opts","threadId","runId","context","state","forwardedProps","supportsToolApproval","supports_tool_approval","referencedConversationIds","referenced_conversation_ids","supportsPageActions","supports_page_actions","supports_artifacts","serialized","buildRequestBody","mentioned_agent_ids","parseEvent","getParser","postCancel","endRelayedTurn","relayedTurn","renderer","endRelayedTurnRef","withdrawRelayedTurn","unfinished","shownUpTo","withdrawRelayedTurnRef","endWithdrawnTurn","endWithdrawnTurnRef","clearResumeWait","endTurnUnseen","withdrawn","endTurnUnseenRef","awaitRelayedResume","awaited","awaitRelayedResumeRef","unnamedTurnFramesRef","handleEventFrame","add","warn","provisionalBubble","lostSight","ends","handleEventFrameRef","cancelHeld","held","resetChat","workspace","clear","stopLocally","running","live","stoppedTurnId","cancelUrl","beforeStart","awaiting_send","none","frameSeq","statusBefore","approvalsBefore","skipWaitUrl","eventsUrl","headersKey","headersKeyRef","eventsRefused","setEventsRefused","eventsReopen","setEventsReopen","connected","retry","hiddenTimer","opened","missed","connect","visibilityState","refused","openedAt","sentHeadersKey","delay","min","random","closeWhileHidden","onVisibilityChange","returnedRef","returnedTick","setReturnedTick","settleReturned","splice","told","tell","takenOver","givenBack","handleSendMessageRef","draftFlushRef","setInterval","clearInterval","cancelled","canSteer","sendsHeld","submitApprovalDecisions","decisions","d","verdict","rejectionReason","rejection_reason","handlePaste","clipboardData","skipWait","cancelHeldMessage","mergeTurnHistory","restored","turnIds","liveId","findIndex","region","shownBefore","unsavedBefore","missing","taken","mergeIntoTurn","settleUnseenEnd","readFor","unseenTurnOutcome","handleNewChat","clearPendingWorkspace","handleStopGenerating","awaitingHandover","handleSwitchConversation","findAgent","useAgents","initialAgent","onAgentChange","agentsDisabled","agentStorageKey","setAgents","agentsLoading","setAgentsLoading","agentsError","setAgentsError","agentsSettled","setAgentsSettled","reloadNonce","setReloadNonce","reloadAgents","selectedAgent","setSelectedAgent","agentMenuOpen","setAgentMenuOpen","requestSeqRef","agentsUrl","seq","isLatest","parseAgents","pickStarting","savedSlug","handleSwitchAgent","onSwitch","parseConversation","messageCount","message_count","SIDEBAR_WIDTH","flashTimer","originalTitle","activeHooks","stopTitleFlash","useAwayCompletionNotice","enabled","onComplete","isViewingChat","wasLoadingRef","startRef","clearOnReturn","hasFocus","wasLoading","unfocused","away","viewingChat","showMessage","startTitleFlash","Notification","permission","notifyOS","useComposerExtras","prompts","setPrompts","quota","setQuota","restLike","promptsPath","quotaPath","promptsUrl","quotaUrl","rawList","description","parsePrompts","quotaNonce","setQuotaNonce","refreshQuota","period","scope","parseQuota","useAgentSuggestions","suggestions","setSuggestions","loading","setLoading","prompt","parseSuggestions","useShallowStable","is","hasOwnProperty","shallowEqual","useLatestCallback","useInsertionEffect","stable","createDictationController","recognition","events","phase","startQueued","disposed","reported","isOn","report","onListeningChange","tryStart","onresult","finalText","interimText","resultIndex","results","isFinal","transcript","phrase","onFinal","onInterim","onerror","onend","toggle","stop","dispose","getSpeechRecognition","SpeechRecognition","webkitSpeechRecognition","TURN_PROGRESS","classifyVoiceFrame","folded","final","attribution","others","segments","RECOGNITION_ROUTINE_ERRORS","createVoiceCall","deps","setTimer","fn","clearTimer","muted","heard","said","armed","speech","speaking","silenceTimer","idleTimer","retryTimer","restartTimer","recognitionRunning","recognitionFailed","recognitionFailures","emit","onChange","wantsRecognition","syncRecognition","setPhase","stopSpeaking","listen","settle","fail","armIdleCheck","probe","handleFrame","present","answer","reply","answerTranscriptLines","speakable","speak","cancelQuestion","continuous","interimResults","lang","interim","spoken","isBargeIn","newPendingAnswer","addressedAgentId","send","onAccepted","ask","interrupt","stopTurn","toggleMute","voiceCallSupported","playAudio","createObjectURL","audio","Audio","played","pause","revokeObjectURL","onended","play","speakInBrowser","speechSynthesis","SpeechSynthesisUtterance","utterance","AlertTriangleIcon","className","_jsxs","xmlns","width","height","viewBox","fill","stroke","strokeWidth","strokeLinecap","strokeLinejoin","_jsx","ArrowRightLeftIcon","AttachFileIcon","BotIcon","x","y","rx","BrainIcon","CheckCircleIcon","cx","cy","CheckIcon","ChevronDownIcon","ChevronRightIcon","CloseIcon","CopyIcon","ry","DatabaseIcon","DefaultLogoIcon","DownloadIcon","points","x1","x2","y1","y2","EditIcon","ExternalLinkIcon","FileIcon","FloatingIcon","FolderIcon","FolderInputIcon","FolderPlusIcon","FullscreenExitIcon","FullscreenIcon","GamepadIcon","GlobeIcon","HistoryIcon","ImageIcon","InfoIcon","MailIcon","MaximizeIcon","MessageSquareIcon","MicIcon","MicOffIcon","PencilIcon","PhoneOffIcon","SearchIcon","SendIcon","SidebarIcon","SparklesIcon","StopCircleIcon","TerminalIcon","ThumbsDownIcon","filled","ThumbsUpIcon","TrashIcon","UserPlusIcon","SPEAKER","VolumeIcon","VolumeOffIcon","WrenchIcon","XCircleIcon","ClearAllConversations","onClear","iconButtonClass","confirming","setConfirming","onClick","Dropdown","open","onClose","anchorRef","placement","panelRef","pos","setPos","top","stableOnClose","handler","active","listener","useClickOutside","useLayoutEffect","rect","getBoundingClientRect","panelHeight","offsetHeight","spaceBelow","innerHeight","bottom","spaceAbove","preferredLeft","innerWidth","portalTarget","createPortal","Spinner","tooltipMaxWidth","opening","maxRight","minLeft","Tooltip","tipRef","setOpening","setPlacement","below","shift","center","minTop","placeTooltip","panel","viewportWidth","tooltipOpening","onMouseEnter","onMouseLeave","onFocus","onBlur","maxWidth","AgentIcon","icon","agentIconKind","modeOptions","mode","getIcon","ChatHeader","transferredFrom","onAgentMenuToggle","onAgentMenuClose","onSwitchAgent","modeMenuOpen","onModeMenuToggle","onModeMenuClose","onModeChange","onNewChat","logoIcon","agentDashboardUrl","historyEnabled","historyMenuOpen","onHistoryMenuToggle","onHistoryMenuClose","conversationsLoading","onSelectConversation","onDeleteConversation","onClearAll","actions","agentAnchorRef","modeAnchorRef","historyAnchorRef","agentQuery","setAgentQuery","filteredAgents","useMemo","showAgentSearch","CurrentModeIcon","autoFocus","onKeyDown","groupMembers","_Fragment","conv","isActive","when","opt","useConversationReferenceSearch","exclude","setState","params","URLSearchParams","referenceSearchUrl","useMentionMenu","onTextChange","textareaRef","onPick","trigger","setTrigger","dismissedAnchor","setDismissedAnchor","activeIndex","setActiveIndex","pendingCaretRef","listboxId","useId","optionId","sync","activeElement","selectionStart","selectionEnd","textBeforeCaret","referenceTrigger","caret","setSelectionRange","setSeen","listed","rememberAnswer","known","dismissed","referenced","local","merged","referenceMenuView","agentCandidates","inserted","after","insertConversationReference","textareaAria","nativeEvent","isComposing","step","shiftKey","SIZE","CIRCUMFERENCE","PI","ROWS","color","ContextUsageIndicator","usage","setOpen","ratio","pruning","compacting","ringColor","textColor","percent","counts","headline","rows","expandable","gauge","strokeDasharray","strokeDashoffset","transform","backgroundColor","borderRadius","MentionMenu","onHover","listRef","scrollIntoView","block","hasAgents","heading","startsConversations","onMouseDown","onMouseMove","PromptPicker","setQuery","QuotaIndicator","quotaPeriodLabel","withScope","compact","exhausted","nearLimit","barColor","NO_REFS","ChatInput","canHold","onCancelHeld","onInputChange","onSend","onStop","onFileAdd","onFileRemove","onPaste","separatorColor","composerToolbar","conversationReferences","fileInputRef","mentionAgents","onPickMention","agentMentionOptions","mentionMenu","revealEndRef","dictation","onFinalText","listening","setListening","setInterim","controllerRef","onFinalTextRef","supported","Ctor","useDictation","trimEnd","scrollHeight","scrollTop","hasToolbar","isFileManagementEnabled","hasContent","hasFilesUploading","hasAttachments","failedFileWaits","canSend","waitsForTurn","steerable","showSteerSend","showHoldSend","footerText","borderTopColor","borderTopWidth","multiple","onKeyUp","maxHeight","disabled","feedbackKeyOf","msg","speechSupported","FORMAT_OPTIONS","time","hour","minute","date","dateWithYear","year","full","dateStyle","timeStyle","formatters","formatter","Intl","DateTimeFormat","browserTimers","KEYS_TOWARD_TOP","singleTouchY","touches","clientY","SCROLLING_OVERFLOW","KIND_ICONS","presentation","video","ACTION","ArtifactCard","onDownload","onRefresh","onOpen","setCurrent","busyFormat","setBusyFormat","gaveUp","setGaveUp","following","isRenderInProgress","intervalMs","forMs","onGiveUp","deadline","pollInTurn","fresh","Icon","kindLabel","artifactKindLabel","facts","renderLabel","videoRenderLabel","link","artifactLink","rel","artifactDownloadLabel","download","formatValue","useRevealIntoView","raf","behavior","inline","BUTTON_BASE","ApprovalCard","proposal","onDecide","rejecting","setRejecting","setReason","reasonId","rejectPanelRef","argumentNames","decide","properties","entry","argumentSchema","ChatApprovalPrompt","onSubmit","isSubmitting","setDecisions","submitted","setSent","busy","submit","all","decidedCount","allDecided","willRunAlways","footerRef","rootRef","ImageLightbox","hostRef","closeButtonRef","root","setRoot","previouslyFocused","HTMLElement","preventScroll","stopPropagation","ChatImage","maxHeightClass","needsAuth","isRelativeUrl","blobUrl","setBlobUrl","errored","setErrored","setExpanded","objectUrl","displaySrc","onError","PLAY_CLASS","ChatWaitingPanel","onPlay","playUrl","keepEndInView","hostMessages","defaultMessages","reducedMotion","reduced","setReduced","matchMedia","mql","addListener","removeListener","usePrefersReducedMotion","msgIndex","setMsgIndex","animation","delegatingLabel","pollingLabel","collectingLabel","resolveStatusVisual","StatusIcon","showDots","rawNames","lower","unique","consultName","targetName","statusLineText","statusLabel","cleanReasoningText","ThinkingTextBubble","isOverflowing","setIsOverflowing","clientHeight","formatElapsed","useSustained","delayMs","firedKey","setFiredKey","ChatThinking","miniGameEnabled","onPlayWaitingGame","waitingGameUrl","onSkipWait","waitingMessages","renderWaitingPanel","visual","tickAnchor","nowMs","setNowMs","waitLeftS","showElapsed","reasoningText","prose","cut","reverse","paragraph","reasoningWindowText","showReasoning","waitedWithoutReasoning","reasoningWentQuiet","showWaiting","ceil","HostRenderBoundary","Component","resetKey","this","props","getDerivedStateFromError","getDerivedStateFromProps","componentDidCatch","componentStack","render","HostRendered","draw","HostRenderFallback","blockParser","holdsDefinition","lineStart","lastIndexOf","settlesBefore","endsIndented","MarkdownBlockSplitter","previous","blocks","starts","settled","firstDefinition","single","reuse","position","splitMarkdownBlocks","MarkdownRenderContext","copiedBlock","onCopyCode","HostCodeBlock","MARKDOWN_COMPONENTS","pre","codeBlockRenderers","codeStr","hostRenderer","ul","ol","li","strong","em","hr","blockquote","internalHref","routeInternally","openInNewTab","isRelativeHref","img","h1","h2","h3","table","tr","th","td","REMARK_PLUGINS","remarkBreaks","MarkdownBody","memo","Markdown","remarkPlugins","urlTransform","components","displayName","StreamedMarkdownBody","splitter","MarkdownMessage","setCopiedBlock","byBlocks","setByBlocks","processedContent","clipboard","writeText","renderContext","prettyTraceValue","toolDisplayName","ToolCallRow","inputDisplay","outputDisplay","hasInput","ReasoningDetailsDialog","dialogRef","dialog","focusable","first","totalCalls","transfers","trace","summaryParts","tn","parentOf","overflowYOf","overflowY","isControl","Element","fileExtensionLabel","dot","ext","IMAGE_EXTENSIONS","SHOWN_ON_HOVER","FOOTER_BUTTON","ACCENT_ON_HOVER","REVEALED_BUTTON","DIMMED_BUTTON","ACTIVE_BUTTON","MessageTime","getFullYear","getMonth","getDate","toISOString","formatMessageTime","dateTime","MessageCopyButton","copied","setCopied","MessageFeedbackButtons","locked","downRef","lockedId","inert","button","FeedbackComment","onSave","onSkip","setText","canSave","ctrlKey","metaKey","ReadAloudButton","onToggle","NO_KINDS","BUILT_IN_ARTIFACT_KINDS","MessageRow","isStreaming","onDownloadFile","resolveAttachmentUrl","onDownloadArtifact","onRefreshArtifact","feedbackMode","onRate","onSaveComment","onSkipComment","canSpeak","isSpeaking","onToggleSpeech","openableConversationIds","onOpenConversation","markerRenderers","markerKindList","transformMessageText","onArtifactOpen","renderMessageExtras","messageFooterActions","setShowReasoning","isAssistant","isEmpty","commentOpen","wasCommentOpen","renderAttachmentCard","att","previewUrl","isImageAttachment","isWorking","sizeLabel","renderFileChip","renderArtifactCard","renderHostMarker","renderWhileStreaming","fullWidth","hasReasoningDetails","showActions","plainText","stripped","stripAnswerMarkers","speechDocuments","session","speechKey","buildUserFileBlocks","chip","attByFileId","artifactsByKey","usedArtifacts","card","FILE","buildAssistantBlocks","ChatMessages","onMessageFeedback","onSubmitApprovalDecisions","transcriptFooter","onLoadEarlier","ttsRoute","scrollRef","renderWindow","setRenderWindow","feedbackEntries","rate","saveComment","skipComment","latest","seqRef","put","patch","change","useMessageFeedback","tts","browserSupported","speakingId","setSpeakingId","localeRef","ttsRef","playbackRef","stopPlayback","playback","synth","createUtterance","finish","gen","onEnd","createSpeechController","speakThroughRoute","reading","useSpeechReader","follower","getBox","timers","lastTop","smooth","jumpToEnd","box","scrollTo","stopWaiting","arrived","reveal","keepUp","onScroll","movedUp","movedDown","distance","leave","createScrollFollower","touchAnchorRef","leaveFrom","container","innerBoxScrollsUp","firstMessageId","prependedRef","loadingEarlier","setLoadingEarlier","prepended","landed","firstId","userMessageCount","seenRef","thinkingLen","hasEarlierMessages","visibleMessages","stopSpeech","spokenStillShown","speechScope","streamingMessageId","awaitingApproval","tabIndex","onWheel","deltaY","abs","deltaX","wheelScrollsUp","currentTarget","altKey","keyScrollsUp","isContentEditable","onPointerDown","press","onControl","onContainer","clientWidth","thumbBottom","pressTakesScrollbar","clientX","clientLeft","clientTop","onTouchStart","onTouchMove","anchorY","swipeScrollsUp","nextTouchAnchor","showEarlier","isStreamingMessage","formatDuration","ChatVoiceCall","processingLabel","onInterrupt","onToggleMute","setSeconds","endRef","tappable","roundButton","Greeting","firstName","hasSlot","at","translateAround","ChatWelcome","promptSuggestions","onPromptClick","agentDescription","suggestionsLoading","fontFamily","DRAG_TYPE","iconButton","sectionLabel","ConversationSidebar","onToggleCollapsed","onSelect","onDelete","onRename","rowActions","headerActions","sections","editingId","setEditingId","draftTitle","setDraftTitle","settledRef","collapsedKey","collapsedGroups","setCollapsedGroups","readCollapsed","creatingWorkspace","setCreatingWorkspace","setError","startRename","commitRename","cancelRename","say","move","isNoopMove","refusal","onMove","grouped","byWorkspace","elsewhere","unfiled","inside","groupConversations","fileable","fileableWorkspaces","unfiledNote","unfiledEmptyNote","renderRow","ConversationRow","conversation","isEditing","onDraftTitleChange","onCommitRename","onCancelRename","onStartRename","NameField","onCreate","onCancel","WorkspaceGroupSection","toggleable","toggleGroup","onDropConversation","DropZone","accepts","over","setOver","carries","dataTransfer","onDragEnter","onDragOver","dropEffect","onDragLeave","onDrop","getData","initial","inputRef","select","commit","renaming","setRenaming","confirmingDelete","setConfirmingDelete","manageable","isMyDefault","moveOpen","setMoveOpen","moveAnchor","movable","draggable","onDragStart","setData","effectAllowed","BACKEND_DOWN_STATUSES","RESTORE_REFUSED_STATUSES","ChatPanel","forwardRef","requests","endedFor","reporter","clone","refusalSessionEnd","ChatPanelBody","topOffset","apiEndpointsProp","waitingGameUrlProp","user","accentColor","promptSuggestionsProp","draftBorderColor","resizable","onWidthChange","onResizeStart","onResizeEnd","disableFileManagement","onRelativeLinkClickProp","onDownloadError","onDownloadErrorProp","requestHeadersProp","pageContextProp","pushContentSelector","onPlayWaitingGameProp","notifyOnComplete","onTaskComplete","onTaskCompleteProp","onMessageFeedbackProp","disableImagePreviews","contextUsageEnabled","onConversationChange","onConversationChangeProp","onAgentChangeProp","readOnlyNotice","getRequestExtrasProp","onStreamEventProp","markerRenderersProp","codeBlockRenderersProp","onArtifactOpenProp","banner","composerHeader","welcome","sidebarRowActions","sidebarHeaderActions","sidebarSections","waitingMessagesProp","onReady","onReadyProp","onBeforeSendProp","onHeldSendSettledProp","onTurnRunningChange","onTurnRunningChangeProp","holdSendsDuringTurn","hideConversationList","upper","markerKinds","isWide","setModeMenuOpen","historySettledId","setHistorySettledId","restoringIdRef","eventsReadyId","setEventsReadyId","eventsGateFor","setEventsGateFor","restorePostponed","setRestorePostponed","restoreOwedId","setRestoreOwedId","restoreAttemptsRef","restoreRefused","setRestoreRefused","refreshConversationsRef","voiceFrameRef","handleStreamFrame","agentSuggestions","defaultSuggestions","refreshConversations","deleteConversation","renameConversation","moveConversation","clearConversations","setConversations","setConversationsLoading","history","clearAllUrl","clearAll","revert","revertMove","useConversations","workspacesEnabled","refreshWorkspaces","createWorkspace","renameWorkspace","deleteWorkspace","setWorkspaces","setAnswered","latestRef","configured","ticket","write","useWorkspaces","pendingId","pendingWorkspaceStillFileable","setHistoryMenuOpen","sidebarCollapsed","setSidebarCollapsed","showConversationSidebar","lastReturnRefreshRef","onReturn","returnRefreshDue","reportedRunningRef","handleSelectConversation","conversationReferencesSearchUrl","threadReferencesConversations","messagesPageBase","serverHasEarlier","setServerHasEarlier","ttsUrl","loadEarlier","oldest","earlier","has_more","applyRestoredHistory","merge","byServerId","inserts","found","mergeReadBack","isLoadingNowRef","onVisible","retryRefusedRestore","refusedRestore","restoringHistory","canDropFiles","dropping","setDropping","dragDepthRef","carriesFiles","dropHandlers","handleClearAll","handleDeleteConversation","sidebarWidth","handleResizeStart","defaultWidth","isResizing","widthStorageKey","setSidebarWidth","parseInt","setIsResizing","isResizingRef","sidebarWidthRef","onWidthChangeRef","onResizeEndRef","handleMouseMove","newWidth","clamped","handleMouseUp","cursor","userSelect","handleWindowResize","useSidebarResize","pushWidth","setProperty","contentElement","originalPaddingRight","paddingRight","originalTransition","transition","resolvedLogo","xtmOneUrl","gameUrl","conversationAgentName","setConversationAgentName","checkVisibility","getClientRects","downloadPathProvided","canDownload","handleDownloadFile","createElement","appendChild","remove","artifactsBase","artifactUrl","handleDownloadArtifact","fallbackName","artifactFileExtension","extended","decodeURIComponent","plain","filenameFromContentDisposition","handleRefreshArtifact","cssVars","mergeTurnHistoryRef","settleUnseenEndRef","isMountedRef","requestedConversationId","isStale","requestedHeadersKey","handleNewChatRef","handleSwitchConversationRef","agentsRef","selectedAgentRef","ready","readyAnnouncedRef","queueRef","queueTick","setQueueTick","enqueue","drop","queueMicrotask","handleSwitchAgentRef","handleStopGeneratingRef","readOnlyRef","draftRef","voice","optionsRef","setSnapshot","callRef","Recognition","hooks","readTurnState","probeTurn","onFrame","useVoiceCall","useImperativeHandle","newConversation","selectConversation","setDraft","getDraft","focusComposer","startVoiceCall","scrollToMessage","dataset","containerClasses","containerStyle","_","ChatToggleButton","isOpen","resolvedIcon","borderColor"],"mappings":"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"}