From: Aleksander Grygier Date: Thu, 13 Aug 2026 06:07:34 +0000 (+0200) Subject: refactor: Clean up UI types (#26909) X-Git-Tag: upstream/0.0.10438~37 X-Git-Url: https://git.djapps.eu/?a=commitdiff_plain;h=a6040c925c3f3e8dce83d51f8761383d2771f4cd;p=pkg%2Fggml%2Fsources%2Fllama.cpp refactor: Clean up UI types (#26909) --- diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index bc1befd63..9921d8d88 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -3,7 +3,7 @@ import { ChatAttachmentsList, ChatFormActions, - ChatFormContenteditable, + ChatFormContentEditable, ChatFormFileInputInvisible, ChatFormMcpResourcesList, ChatFormPickers, @@ -605,7 +605,7 @@ class="flex-column relative min-h-12 items-center rounded-4xl md:rounded-3xl py-2 pb-2.25 shadow-sm transition-all focus-within:shadow-md md:py-3!" > {#if useContenteditable} - + import { CODE_BLOCK } from '$lib/constants'; + import { ColorMode } from '$lib/enums'; + import { isMobile } from '$lib/stores/viewport.svelte'; + import type { ContentEditableToken } from '$lib/types'; + import type { SourceHistoryEntry } from '$lib/utils'; + import { + badgeAwareWordJump, + buildFragment, + domMatchesTokens, + highlightCode, + isIMEComposing, + isOffsetInCodeBlock, + leadingBadgeEdgeOffset, + rangeToTextOffset, + serializeContent, + SourceHistory, + stripBlockBoundaryLineBreaks, + syncCodeBlockHatches, + textOffsetToRange, + tokenizeContent + } from '$lib/utils'; + import githubLightCss from 'highlight.js/styles/github.css?inline'; + import githubDarkCss from 'highlight.js/styles/github-dark.css?inline'; + import { mode } from 'mode-watcher'; + import { onDestroy, onMount, untrack } from 'svelte'; + + interface Props { + class?: string; + disabled?: boolean; + onInput?: () => void; + onKeydown?: (event: KeyboardEvent) => void; + onPaste?: (event: ClipboardEvent) => void; + placeholder?: string; + value?: string; + } + + let { + class: className = '', + disabled = false, + onInput, + onKeydown, + onPaste, + placeholder = 'Ask anything...', + value = $bindable('') + }: Props = $props(); + + let rootElement: HTMLDivElement | undefined = $state(); + let lastEmittedValue = ''; + let isComposing = $state(false); + + // Undo/redo in source space: the imperative token rebuilds destroy the + // browser's native undo stack. + const history = new SourceHistory(); + + // Browsers disagree on what an empty contenteditable contains (`
`, + // `

`, or nothing), so emptiness is decided by the + // serialized source, not the DOM shape. + function syncEmptyState(serialized?: string) { + if (!rootElement) return; + + const source = serialized ?? serializeContent(rootElement); + + rootElement.dataset.empty = source.length === 0 ? 'true' : 'false'; + } + + function renderTokens(tokens: ContentEditableToken[]) { + if (!rootElement) return; + + const caret = rangeToTextOffset(rootElement, safeRange()); + + // eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children + rootElement.replaceChildren(buildFragment(tokens)); + + syncCodeBlockHatches(rootElement); + highlightCodeBlocks(rootElement); + + restoreCaret(caret); + resizeHeight(); + syncEmptyState(); + } + + // Last highlighted source segment per block element - typing inside + // a block re-highlights only when the segment actually changed. + const highlightedSegments = new WeakMap(); + + const CODE_BLOCK_OPEN_RE = /^```([^\n`]*)\n/; + + /** + * Apply syntax highlighting to a code block element's CONTENT. The + * fence lines stay plain text, and the blank padding that + * `highlightCode` trims is re-added as plain text, so the element's + * textContent stays byte-exact with the source segment. Replaces + * the element's children - callers restore the caret afterwards. + * Returns false when nothing changed. + */ + function highlightCodeBlockElement(el: HTMLElement): boolean { + const segment = el.textContent ?? ''; + + if (highlightedSegments.get(el) === segment) return false; + + const open = CODE_BLOCK_OPEN_RE.exec(segment); + + if (!open) return false; + + const prefix = open[0]; + const language = open[1].trim().split(/\s+/)[0] ?? ''; + const content = segment.slice(prefix.length, -3); + const leading = content.match(CODE_BLOCK.TRIM_LEADING_PADDING_REGEX)?.[0] ?? ''; + const trailing = content.match(CODE_BLOCK.TRIM_TRAILING_PADDING_REGEX)?.[0] ?? ''; + const core = content.slice(leading.length, content.length - trailing.length); + // autoDetect off: re-guessing the language on every keystroke + // costs ~38ms a call and flickers while typing + const html = core ? highlightCode(core, language || 'text', false) : ''; + const tpl = document.createElement('template'); + + tpl.innerHTML = html; + + el.replaceChildren( + document.createTextNode(prefix + leading), + tpl.content.cloneNode(true), + document.createTextNode(trailing + '```') + ); + highlightedSegments.set(el, segment); + + return true; + } + + function highlightCodeBlocks(root: HTMLElement) { + for (const el of root.querySelectorAll('code[data-code-token="block"]')) { + highlightCodeBlockElement(el); + } + } + + /** + * Re-highlight the code block the caret sits in after an edit. + * Skipped when the block's segment is unchanged since its last + * highlight, so edits outside blocks cost nothing. + */ + function rehighlightCaretCodeBlock() { + if (!rootElement) return; + + const range = safeRange(); + + if (!range) return; + + let node: Node | null = range.startContainer; + + if (node === rootElement) { + node = rootElement.childNodes[range.startOffset - 1] ?? null; + } + + while (node && node !== rootElement) { + if (node instanceof HTMLElement && node.dataset.codeToken === 'block') { + const caret = rangeToTextOffset(rootElement, range); + + if (highlightCodeBlockElement(node)) { + restoreCaret(caret); + } + + return; + } + + node = node.parentNode; + } + } + + /** + * Is the caret inside a fenced code block region? Source-level + * (not DOM-level) so the still-OPEN fence counts too: while the + * user is typing a block, no closing ``` exists yet and the + * buffer is plain text with no block element to find. Root-level + * caret positions right at a closed block's edge (escape + * hatches, element boundaries restored by `textOffsetToRange`) + * resolve past the closing fence, so they count as OUTSIDE. + */ + function caretInCodeBlock(): boolean { + if (!rootElement) return false; + + return isOffsetInCodeBlock( + serializeContent(rootElement), + rangeToTextOffset(rootElement, safeRange()) + ); + } + + /** + * hljs theme for the highlighted code blocks. Mirrors + * SyntaxHighlightedCode.svelte: one shared style element + * (deduped via the data attribute) swapped on mode change. + */ + function loadHighlightTheme(isDark: boolean) { + document.querySelectorAll('style[data-highlight-theme-preview]').forEach((s) => s.remove()); + + const style = document.createElement('style'); + + style.setAttribute('data-highlight-theme-preview', 'true'); + style.textContent = isDark ? githubDarkCss : githubLightCss; + + document.head.appendChild(style); + } + + $effect(() => { + loadHighlightTheme(mode.current === ColorMode.DARK); + }); + + function safeRange(): Range | null { + if (!rootElement) return null; + + const selection = window.getSelection(); + + if (!selection || selection.rangeCount === 0) return null; + + const range = selection.getRangeAt(0); + + if (!rootElement.contains(range.startContainer) || !rootElement.contains(range.endContainer)) { + return null; + } + + return range; + } + + function restoreCaret(offset: number, extend = false) { + if (!rootElement) return; + + const target = textOffsetToRange(rootElement, offset); + const selection = window.getSelection(); + + if (!selection) return; + + if (extend && selection.anchorNode) { + selection.setBaseAndExtent( + selection.anchorNode, + selection.anchorOffset, + target.startContainer, + target.startOffset + ); + + return; + } + + selection.removeAllRanges(); + selection.addRange(target); + } + + function resizeHeight() { + if (!rootElement) return; + + rootElement.style.height = 'auto'; + rootElement.style.height = `${rootElement.scrollHeight}px`; + } + + function recordHistory(newGroup: boolean) { + if (!rootElement) return; + + history.push( + { caret: rangeToTextOffset(rootElement, safeRange()), value: lastEmittedValue }, + Date.now(), + newGroup + ); + } + + /** + * Re-emit the current markdown source value to the parent, then + * reconcile the DOM against the token stream: when a code span + * was just completed or broken, the token boundaries no longer + * match the element structure and the DOM is rebuilt (caret + * preserved through the source-offset mapping). + */ + function processInput(inputType?: string) { + if (isComposing || !rootElement) return; + + syncEmptyState(); + resizeHeight(); + + // Shift+Enter right after a code block leaves an all-newline + // text node (the fence's separator line plus Chromium's + // artificial end-of-buffer line break). Strip both so the caret + // lands on the line directly below the block. + if (inputType === 'insertLineBreak' || inputType === 'insertParagraph') { + const caret = rangeToTextOffset(rootElement, safeRange()); + + if (stripBlockBoundaryLineBreaks(rootElement)) { + restoreCaret(caret); + } else { + const source = serializeContent(rootElement); + + let end = caret; + + // the caret must end up after the inserted \n; some browsers + // leave it before (stuck at the end of the old line). A + // preceding \n means it already sits past the break + // (Chromium's artificial trailing newline) - leave it. + if (source[end] === '\n' && source[end - 1] !== '\n') { + end += 1; + restoreCaret(end); + } + + // a line break at the buffer end renders only with a second, + // artificial trailing \n: a lone trailing \n is collapsed, so + // the new line is invisible and the next typed character + // consumes it. Append it when missing - unless the trailing + // \n doubles as a block's separator line (source ends with + // \n\n) or sits inside a block element. + let last = rootElement.lastChild; + + while (last && last.nodeName === 'BR') last = last.previousSibling; + + if ( + end === source.length && + source.endsWith('\n') && + source[source.length - 2] !== '\n' && + last?.nodeType === Node.TEXT_NODE + ) { + // eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children + rootElement.appendChild(document.createTextNode('\n')); + restoreCaret(source.length); + resizeHeight(); + } + } + } + + syncCodeBlockHatches(rootElement); + + const serialized = serializeContent(rootElement); + + syncEmptyState(serialized); + + if (serialized === lastEmittedValue) return; + + // Plain typing/deletes coalesce per time window; structural edits + // (paste, newline, cut, autocorrect) start a new undo group. + recordHistory(inputType !== 'insertText' && !inputType?.startsWith('deleteContent')); + + lastEmittedValue = serialized; + value = serialized; + + // Rebuild when token boundaries shifted (a code span was just + // completed or broken) - the browser-owned text nodes cannot + // restyle themselves across element boundaries. + const tokens = tokenizeContent(serialized); + + if (!domMatchesTokens(rootElement, tokens)) { + renderTokens(tokens); + + // The rebuild can re-shape the DOM in a way that changes the + // serialization (e.g. Chromium merged trailing text into the + // block element and the rebuild splits it back out, which + // synthesizes the separator newline) - keep value in sync. + const reserialized = serializeContent(rootElement); + + if (reserialized !== serialized) { + lastEmittedValue = reserialized; + value = reserialized; + } + } else { + rehighlightCaretCodeBlock(); + } + + onInput?.(); + } + + function handleInput(event: Event) { + processInput((event as InputEvent).inputType); + } + + function handleCompositionStart() { + isComposing = true; + } + + function handleCompositionEnd() { + isComposing = false; + processInput(); + } + + /** + * Insert a line break at the caret MANUALLY. Native Shift+Enter at + * the buffer end varies across browsers (a lone trailing \n that the + * renderer collapses, or a
that the hatch sync strips), which + * can leave the caret stuck on the old line; splitting the text node + * ourselves keeps the DOM shape - and the caret - deterministic. + * `processInput` then appends the artificial trailing \n when the + * break lands at the buffer end. + */ + function insertLineBreak() { + if (!rootElement) return; + + const range = safeRange(); + + if (!range) return; + + if (!range.collapsed) { + range.deleteContents(); + } + + const container = range.startContainer; + const offset = range.startOffset; + const nl = document.createTextNode('\n'); + + // a break at the very end of a code block exits the block (the + // new line belongs below it, not inside) + let exitBlock: HTMLElement | null = null; + + if (container.nodeType === Node.TEXT_NODE) { + let node: Node | null = container.parentNode; + + while (node && node !== rootElement) { + if (node instanceof HTMLElement && node.dataset.codeToken === 'block') { + const tail = document.createRange(); + + tail.setStart(container, offset); + tail.setEnd(node, node.childNodes.length); + + if (tail.toString().length === 0) exitBlock = node; + + break; + } + + node = node.parentNode; + } + } + + if (exitBlock) { + exitBlock.after(nl); + } else if (container.nodeType === Node.TEXT_NODE) { + const text = container as Text; + + if (offset === 0) { + text.before(nl); + } else if (offset === text.length) { + text.after(nl); + } else { + text.splitText(offset).before(nl); + } + } else { + container.insertBefore(nl, container.childNodes[offset] ?? null); + } + + const selection = window.getSelection(); + const after = document.createRange(); + + after.setStartAfter(nl); + after.collapse(true); + selection?.removeAllRanges(); + selection?.addRange(after); + + processInput('insertLineBreak'); + } + + /** + * Arrow escape to the line BEFORE a leading code block. Native + * caret movement has no position above a buffer-starting block, + * so a transient `
` hatch is created on demand: it gives the + * caret a visible line, is consumed by the first character typed + * on it, and is removed again when the caret leaves (see + * handleSelectionChange). Returns true when the caret was moved. + */ + function moveCaretBeforeLeadingCodeBlock(key: string, extend: boolean): boolean { + if (!rootElement) return false; + + // a hatch already exists - native movement handles it + if (rootElement.firstChild?.nodeName === 'BR') return false; + + const first = rootElement.firstChild; + + if (!(first instanceof HTMLElement) || first.dataset.codeToken !== 'block') return false; + + const range = safeRange(); + + if (!range || !range.collapsed) return false; + + // the caret must sit inside the block: on its very first + // character for ArrowLeft, anywhere on its first line for + // ArrowUp + if (!first.contains(range.startContainer)) return false; + + const caret = rangeToTextOffset(rootElement, range); + + if (key === 'ArrowLeft') { + if (caret !== 0) return false; + } else { + const firstLineEnd = (first.textContent ?? '').indexOf('\n'); + + if (firstLineEnd !== -1 && caret > firstLineEnd) return false; + } + + // eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children + rootElement.prepend(document.createElement('br')); + restoreCaret(0, extend); + + return true; + } + + /** + * Remove the transient leading hatch once the caret leaves it. + * The hatch only exists to give the caret a line above a leading + * code block; with the caret anywhere else the empty line would + * just be visual noise. Typing on the hatch line consumes it via + * the stale-hatch removal in `syncCodeBlockHatches` instead (the + * new text node takes its place before the block). + */ + function handleSelectionChange() { + if (!rootElement) return; + + const first = rootElement.firstChild; + + if (first?.nodeName !== 'BR') return; + + const second = first.nextSibling; + + if (!(second instanceof HTMLElement) || second.dataset.codeToken !== 'block') return; + + const range = safeRange(); + const onHatch = + range !== null && range.startContainer === rootElement && range.startOffset === 0; + + if (!onHatch) { + first.remove(); + } + } + + /** + * Undo/redo is replayed from source snapshots (the token rebuilds + * destroy the native undo stack). Arrow keys around badges are + * repaired locally: a badge is a non-editable island, so plain + * ArrowLeft after a leading badge has no native previous position + * and word jumps overshoot it by a word. + * + * Plain Enter inside a fenced code block (closed, or still open + * while being typed) acts as Shift+Enter and adds a line instead of + * submitting. ArrowLeft/ArrowUp at the edge of a leading code block + * create the transient before-block hatch. + */ + function handleKeydown(event: KeyboardEvent) { + const mod = event.ctrlKey || event.metaKey; + + if (mod && !event.altKey && !isComposing && rootElement) { + const key = event.key.toLowerCase(); + const isUndo = key === 'z' && !event.shiftKey; + const isRedo = key === 'y' || (key === 'z' && event.shiftKey); + + if (isUndo || isRedo) { + event.preventDefault(); + const current = { + caret: rangeToTextOffset(rootElement, safeRange()), + value: lastEmittedValue + }; + const entry = isUndo ? history.undo(current) : history.redo(current); + + if (entry) applyHistoryEntry(entry); + + return; + } + } + + if ( + event.key === 'Enter' && + event.shiftKey && + !event.ctrlKey && + !event.metaKey && + !event.altKey && + !isIMEComposing(event) && + !disabled && + !caretInCodeBlock() && + safeRange() + ) { + // Own the break outside code blocks: native end-of-buffer + // behavior varies across browsers and can leave the caret + // stuck on the old line (see insertLineBreak). + event.preventDefault(); + insertLineBreak(); + + return; + } + + if ( + event.key === 'Enter' && + !event.shiftKey && + !event.ctrlKey && + !event.metaKey && + !event.altKey && + !isIMEComposing(event) && + caretInCodeBlock() + ) { + // The native plain-Enter path must never run: it splits the + // buffer into `
` wrappers that `serializeContent` cannot + // see. `insertLineBreak` reproduces the Shift+Enter DOM (a `\n` + // text node) and fires `input` synchronously, so the usual + // re-tokenize/re-highlight follows. + event.preventDefault(); + document.execCommand('insertLineBreak'); + + return; + } + + if ( + rootElement && + (event.key === 'ArrowLeft' || event.key === 'ArrowUp') && + !event.altKey && + !event.ctrlKey && + !event.metaKey + ) { + if (moveCaretBeforeLeadingCodeBlock(event.key, event.shiftKey)) { + event.preventDefault(); + + return; + } + } + + if (rootElement && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) { + const isWordJump = (event.altKey || event.ctrlKey) && !event.metaKey; + const isPlainLeft = + event.key === 'ArrowLeft' && !event.altKey && !event.ctrlKey && !event.metaKey; + + if (isWordJump || isPlainLeft) { + const source = serializeContent(rootElement); + const caret = rangeToTextOffset(rootElement, safeRange()); + const target = isWordJump + ? badgeAwareWordJump(source, caret, event.key === 'ArrowRight' ? 'forward' : 'backward') + : leadingBadgeEdgeOffset(source, caret); + + if (target !== null) { + event.preventDefault(); + restoreCaret(target, event.shiftKey); + + return; + } + } + } + + onKeydown?.(event); + } + + // lastEmittedValue is set before `value` so the sync effect treats the + // change as our own and does not re-render. + function applyHistoryEntry(entry: SourceHistoryEntry) { + if (!rootElement) return; + + renderTokens(tokenizeContent(entry.value)); + lastEmittedValue = entry.value; + value = entry.value; + onInput?.(); + restoreCaret(entry.caret); + } + + /** + * Plain-text paste. preventDefault + manual insertText keeps the + * browser from producing stray `
` wrappers mid-paste; insertText + * fires `input` synchronously, so `processInput` re-tokenizes the + * buffer and rebuilds when the pasted text carries badge or code + * tokens. + */ + function handlePasteEvent(event: ClipboardEvent) { + const pasted = event.clipboardData?.getData('text/plain'); + + if (pasted && pasted.length > 0) { + event.preventDefault(); + + // Snap a collapsed caret through the offset mapping first: at + // element-boundary carets (e.g. right before a badge) Chromium's + // insertText can drop the preceding text node's trailing whitespace. + const range = safeRange(); + + if (rootElement && range && range.collapsed) { + restoreCaret(rangeToTextOffset(rootElement, range)); + } + + document.execCommand('insertText', false, pasted); + } + } + + // The parent's paste handler runs first and preventDefaults when it + // consumes the event (files, quoted prompts, long text). + function handlePaste(event: ClipboardEvent) { + onPaste?.(event); + + if (!event.defaultPrevented) { + handlePasteEvent(event); + } + } + + // The selection as markdown SOURCE (each badge contributes its full + // `[name](file://...)` link), so copy/cut carry raw markdown and + // pasting back re-renders the badges. Null for collapsed/outside + // selections - native clipboard behavior is fine there. + function selectionSourceSlice(): { text: string; range: Range } | null { + if (!rootElement) return null; + + const range = safeRange(); + + if (!range || range.collapsed) return null; + + const startRange = range.cloneRange(); + + startRange.collapse(true); + + const source = serializeContent(rootElement); + const start = rangeToTextOffset(rootElement, startRange); + const end = rangeToTextOffset(rootElement, range); + + return { range, text: source.slice(start, end) }; + } + + function handleCopy(event: ClipboardEvent) { + const slice = selectionSourceSlice(); + + if (!slice) return; + + event.clipboardData?.setData('text/plain', slice.text); + event.preventDefault(); + } + + function handleCut(event: ClipboardEvent) { + const slice = selectionSourceSlice(); + + if (!slice) return; + + event.clipboardData?.setData('text/plain', slice.text); + event.preventDefault(); + + // preventDefault suppresses the native deletion, so remove the + // selection manually and re-emit. + slice.range.deleteContents(); + processInput('deleteByCut'); + } + + onMount(() => { + // untrack: the DOM is managed manually from input events, so the + // initial render must not subscribe to the value. + renderTokens(tokenizeContent(untrack(() => value))); + lastEmittedValue = untrack(() => value ?? ''); + resizeHeight(); + syncEmptyState(); + document.addEventListener('selectionchange', handleSelectionChange); + + if (!isMobile.current) { + rootElement?.focus({ preventScroll: true }); + } + }); + + onDestroy(() => { + document.removeEventListener('selectionchange', handleSelectionChange); + }); + + // External `value` updates. When incoming === lastEmittedValue the + // change came from our own input, so leave the DOM alone - the + // browser already owns the right shape. + $effect(() => { + const incoming = value ?? ''; + + if (incoming === lastEmittedValue) return; + + recordHistory(true); // external edit (mention insert, clear, ...): own undo step + renderTokens(tokenizeContent(incoming)); + lastEmittedValue = incoming; + }); + + export function getElement() { + return rootElement; + } + + export function getCaretOffset(): number { + if (!rootElement) return 0; + + return rangeToTextOffset(rootElement, safeRange()); + } + + // Focus first: `selection.addRange` requires it on some browsers. + export function setCaretOffset(offset: number) { + if (rootElement && rootElement !== document.activeElement) { + rootElement.focus({ preventScroll: true }); + } + + restoreCaret(offset); + } + + export function focus() { + if (isMobile.current) return; + + rootElement?.focus({ preventScroll: true }); + } + + export function resetHeight() { + if (rootElement) { + rootElement.style.height = ''; + resizeHeight(); + } + } + + +
+
+
+ + diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte deleted file mode 100644 index ec916b3f1..000000000 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte +++ /dev/null @@ -1,848 +0,0 @@ - - -
-
-
- - diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugeDial.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugeDial.svelte index 8f3ec4352..67d705ae4 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugeDial.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugeDial.svelte @@ -1,6 +1,6 @@ + + diff --git a/tools/ui/tests/client/components/ChatFormContenteditableHarness.svelte b/tools/ui/tests/client/components/ChatFormContenteditableHarness.svelte deleted file mode 100644 index fac773756..000000000 --- a/tools/ui/tests/client/components/ChatFormContenteditableHarness.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - diff --git a/tools/ui/tests/unit/assistant-raw-output.test.ts b/tools/ui/tests/unit/assistant-raw-output.test.ts index ec75a0e49..d13b796c4 100644 --- a/tools/ui/tests/unit/assistant-raw-output.test.ts +++ b/tools/ui/tests/unit/assistant-raw-output.test.ts @@ -1,6 +1,7 @@ import { REASONING_TAGS } from '$lib/constants'; import { AgenticSectionType } from '$lib/enums'; -import { type AgenticSection, buildAssistantRawOutput } from '$lib/utils/agentic'; +import type { AgenticSection } from '$lib/types/agentic'; +import { buildAssistantRawOutput } from '$lib/utils/agentic'; import { describe, expect, it } from 'vitest'; function makeSection( diff --git a/tools/ui/tests/unit/tool-calls.test.ts b/tools/ui/tests/unit/tool-calls.test.ts index 3349b7ab6..b0ec76be4 100644 --- a/tools/ui/tests/unit/tool-calls.test.ts +++ b/tools/ui/tests/unit/tool-calls.test.ts @@ -10,7 +10,7 @@ import { type WriteFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file'; import { AgenticSectionType, BuiltInTool } from '$lib/enums'; -import type { AgenticSection } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; import { abbreviateHome, formatCwdMessage, lastPathSegment, parseCwdMessage } from '$lib/utils'; import { describe, expect, it } from 'vitest';