From b63c6ef1103b23f446c7fb48a4d5e367cb686416 Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Sun, 24 May 2026 14:36:03 +0700 Subject: [PATCH 01/14] webui: added single line reasoning preview. --- .../ChatMessageAgenticContent.svelte | 29 ++++++++++- .../content/CollapsibleContentBlock.svelte | 26 +++++++--- tools/ui/src/lib/constants/markdown.ts | 22 ++++++++ tools/ui/src/lib/utils/formatters.ts | 50 ++++++++++++++++++- tools/ui/src/lib/utils/index.ts | 3 +- 5 files changed, 118 insertions(+), 12 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 3a9cc7e9356d..f26dd7bb851c 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -22,6 +22,7 @@ } from '$lib/types'; import { deriveAgenticSections, + formatReasoningPreview, formatJsonPretty, parseToolResultWithImages, type AgenticSection, @@ -31,7 +32,8 @@ agenticPendingPermissionRequest, agenticResolvePermission, agenticPendingContinueRequest, - agenticResolveContinue + agenticResolveContinue, + agenticLastError } from '$lib/stores/agentic.svelte'; import { config } from '$lib/stores/settings.svelte'; @@ -56,6 +58,18 @@ const showToolCallInProgress = $derived(config().showToolCallInProgress as boolean); const showThoughtInProgress = $derived(config().showThoughtInProgress as boolean); + const hasReasoningError = $derived( + isLastAssistantMessage ? !!agenticLastError(message.convId) : false + ); + + const isReasoningInterrupted = $derived( + isLastAssistantMessage && + !isStreaming && + !!message.reasoningContent && + !message.content?.trim() && + !message.toolCalls + ); + let permissionDismissed = $state(false); const pendingPermission = $derived( @@ -293,11 +307,20 @@ {:else if section.type === AgenticSectionType.REASONING} + {@const reasoningPreview = formatReasoningPreview(section.content)} + {@const reasoningSubtitle = isReasoningInterrupted + ? hasReasoningError + ? 'Error' + : 'Cancelled' + : (isStreaming ? '' : undefined)} + toggleExpanded(index, section)} >
@@ -307,8 +330,9 @@
{:else if section.type === AgenticSectionType.REASONING_PENDING} + {@const reasoningPreview = formatReasoningPreview(section.content)} {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'} - {@const reasoningSubtitle = isStreaming ? '' : 'incomplete'} + {@const reasoningSubtitle = isStreaming ? '' : (hasReasoningError ? 'Error' : 'Cancelled')} toggleExpanded(index, section)} > diff --git a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte index b7297ab6b1aa..a55134467ee7 100644 --- a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte +++ b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte @@ -14,6 +14,7 @@ iconClass?: string; title: string; subtitle?: string; + preview?: string; isStreaming?: boolean; onToggle?: () => void; children: Snippet; @@ -26,6 +27,7 @@ iconClass = 'h-4 w-4', title, subtitle, + preview, isStreaming = false, onToggle, children @@ -58,16 +60,24 @@ class={className} > - -
- {#if IconComponent} - - {/if} + +
+
+ {#if IconComponent} + + {/if} + + {title} - {title} + {#if subtitle} + {subtitle} + {/if} +
- {#if subtitle} - {subtitle} + {#if preview} +
{preview}
{/if}
diff --git a/tools/ui/src/lib/constants/markdown.ts b/tools/ui/src/lib/constants/markdown.ts index 783d31a22c11..c4d1124498d7 100644 --- a/tools/ui/src/lib/constants/markdown.ts +++ b/tools/ui/src/lib/constants/markdown.ts @@ -2,3 +2,25 @@ export const IMAGE_NOT_ERROR_BOUND_SELECTOR = 'img:not([data-error-bound])'; export const DATA_ERROR_BOUND_ATTR = 'errorBound'; export const DATA_ERROR_HANDLED_ATTR = 'errorHandled'; export const BOOL_TRUE_STRING = 'true'; +export const STRIP_MARKDOWN_PATTERNS: [RegExp, string | ((match: string) => string)][] = [ + // Code blocks (fenced and inline) + [/```[\s\S]*?```/g, ''], + [/`[^`]*`/g, ''], + // HTML tags + [/<[^>]*>/g, ''], + // Bold and italic markers + [/\*\*(.*?)\*\*/g, '$1'], + [/__(.*?)__/g, '$1'], + [/\*(.*?)\*/g, '$1'], + [/_(.*?)_/g, '$1'], + // Blockquotes, headings, and list markers + [/^>\s*/gm, ''], + [/^#{1,6}\s+/gm, ''], + [/^[\s]*[-*+]\s+/gm, ''], + [/^[\s]*\d+[.)]\s+/gm, ''], + // Emoji + [ + /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{231A}-\u{231B}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}]/gu, + '' + ] +]; \ No newline at end of file diff --git a/tools/ui/src/lib/utils/formatters.ts b/tools/ui/src/lib/utils/formatters.ts index 24a2c1c94c18..8b6f9bdefea2 100644 --- a/tools/ui/src/lib/utils/formatters.ts +++ b/tools/ui/src/lib/utils/formatters.ts @@ -3,7 +3,8 @@ import { SECONDS_PER_MINUTE, SECONDS_PER_HOUR, SHORT_DURATION_THRESHOLD, - MEDIUM_DURATION_THRESHOLD + MEDIUM_DURATION_THRESHOLD, + STRIP_MARKDOWN_PATTERNS } from '$lib/constants'; /** @@ -151,3 +152,50 @@ export function formatAttachmentText( const header = extra ? `${name} (${extra})` : name; return `\n\n--- ${label}: ${header} ---\n${content}`; } + +/** + * Strips markdown formatting (code blocks, bold, italic, links, etc.) + * and emoji characters from text, leaving plain readable content. + * Do note that the regex array isn't an exhaustive list + * + * @param text - Raw text that may contain markdown or emoji + * @returns Plain text with all formatting and emoji removed + */ +function stripMarkdownAndEmoji(text: string): string { + let result = text; + + for (const [pattern, replacement] of STRIP_MARKDOWN_PATTERNS) { + result = result.replace(pattern, replacement as string); + } + + return result.trim(); +} + +/** + * Derives a compact preview string from reasoning content for use in collapsed UI. + * + * Edge cases handled: + * - Markdown and emoji are stripped before processing + * - If the first non-empty line is shorter than 30 characters, + * the second non-empty line is appended with a " - " separator + * - Returns empty string if no content remains after stripping + * + * @param content - Raw reasoning content (may contain markdown/emoji) + * @returns A short, clean preview string + */ +export function formatReasoningPreview(content: string): string { + if (!content) return ''; + + const clean = stripMarkdownAndEmoji(content); + const lines = clean.split('\n').filter((l) => l.trim().length > 0); + + if (lines.length === 0) return ''; + + let preview = lines[0].trim(); + + if (preview.length < 30 && lines.length > 1) { + preview = preview + ' - ' + lines[1].trim(); + } + + return preview; +} diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts index 00aa49c41765..fef44310152a 100644 --- a/tools/ui/src/lib/utils/index.ts +++ b/tools/ui/src/lib/utils/index.ts @@ -76,7 +76,8 @@ export { formatJsonPretty, formatTime, formatPerformanceTime, - formatAttachmentText + formatAttachmentText, + formatReasoningPreview, } from './formatters'; // IME utilities From c521b938f1a2127353755818d2dee2f521654653 Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Sun, 24 May 2026 15:19:04 +0700 Subject: [PATCH 02/14] patch: reduce width slightly for the previewing section --- .../lib/components/app/content/CollapsibleContentBlock.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte index a55134467ee7..f001cf4688cc 100644 --- a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte +++ b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte @@ -77,7 +77,7 @@
{#if preview} -
{preview}
+
{preview}
{/if} From 6118b83e98ecb951b39df7c530d43a38817a9818 Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Sun, 24 May 2026 23:59:27 +0700 Subject: [PATCH 03/14] refactor: move formatter constants to the right file --- tools/ui/src/lib/constants/formatters.ts | 32 ++++++++++++++++++++++++ tools/ui/src/lib/constants/markdown.ts | 22 ---------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/tools/ui/src/lib/constants/formatters.ts b/tools/ui/src/lib/constants/formatters.ts index d6d1b883ffe9..c84bc410dd71 100644 --- a/tools/ui/src/lib/constants/formatters.ts +++ b/tools/ui/src/lib/constants/formatters.ts @@ -6,3 +6,35 @@ export const MEDIUM_DURATION_THRESHOLD = 10; /** Default display value when no performance time is available */ export const DEFAULT_PERFORMANCE_TIME = '0s'; + +/** Max length before reasoning preview is truncated */ +export const MAX_PREVIEW_LENGTH = 120; + +/** + * Regex patterns used to strip markdown formatting from text. + * Each entry is [pattern, replacement] — replacement can be a string + * or a function that receives the match and returns the replacement. + */ +export const STRIP_MARKDOWN_PATTERNS: [RegExp, string | ((match: string) => string)][] = [ + // Code blocks (fenced and inline) + [/^```.*/gm, ''], + [/.*```$/gm, ''], + [/`[^`]*`/g, ''], + // HTML tags + [/<[^>]*>/g, ''], + // Bold and italic markers + [/\*\*(.*?)\*\*/g, '$1'], + [/__(.*?)__/g, '$1'], + [/\*(.*?)\*/g, '$1'], + [/_(.*?)_/g, '$1'], + // Blockquotes, headings, and list markers + [/^>\s*/gm, ''], + [/^#{1,6}\s+/gm, ''], + [/^[\s]*[-*+]\s+/gm, ''], + [/^[\s]*\d+[.)]\s+/gm, ''], + // Emoji + [ + /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{231A}-\u{231B}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}]/gu, + '' + ] +]; diff --git a/tools/ui/src/lib/constants/markdown.ts b/tools/ui/src/lib/constants/markdown.ts index c4d1124498d7..783d31a22c11 100644 --- a/tools/ui/src/lib/constants/markdown.ts +++ b/tools/ui/src/lib/constants/markdown.ts @@ -2,25 +2,3 @@ export const IMAGE_NOT_ERROR_BOUND_SELECTOR = 'img:not([data-error-bound])'; export const DATA_ERROR_BOUND_ATTR = 'errorBound'; export const DATA_ERROR_HANDLED_ATTR = 'errorHandled'; export const BOOL_TRUE_STRING = 'true'; -export const STRIP_MARKDOWN_PATTERNS: [RegExp, string | ((match: string) => string)][] = [ - // Code blocks (fenced and inline) - [/```[\s\S]*?```/g, ''], - [/`[^`]*`/g, ''], - // HTML tags - [/<[^>]*>/g, ''], - // Bold and italic markers - [/\*\*(.*?)\*\*/g, '$1'], - [/__(.*?)__/g, '$1'], - [/\*(.*?)\*/g, '$1'], - [/_(.*?)_/g, '$1'], - // Blockquotes, headings, and list markers - [/^>\s*/gm, ''], - [/^#{1,6}\s+/gm, ''], - [/^[\s]*[-*+]\s+/gm, ''], - [/^[\s]*\d+[.)]\s+/gm, ''], - // Emoji - [ - /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{231A}-\u{231B}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}]/gu, - '' - ] -]; \ No newline at end of file From ad84705663ea859301411405b4942bb7e51a79bb Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Mon, 25 May 2026 00:02:52 +0700 Subject: [PATCH 04/14] feat: reimplement reasoning preview with throttled dynamic per-line rendering --- .../ChatMessageAgenticContent.svelte | 6 +- .../content/CollapsibleContentBlock.svelte | 24 +++++-- tools/ui/src/lib/hooks/use-throttle.svelte.ts | 32 ++++++++++ tools/ui/src/lib/utils/formatters.ts | 64 ++++++++----------- 4 files changed, 81 insertions(+), 45 deletions(-) create mode 100644 tools/ui/src/lib/hooks/use-throttle.svelte.ts diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index f26dd7bb851c..3581555a26e4 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -307,7 +307,7 @@
{:else if section.type === AgenticSectionType.REASONING} - {@const reasoningPreview = formatReasoningPreview(section.content)} + {@const { preview: reasoningPreview, overflow: reasoningOverflow } = formatReasoningPreview(section.content)} {@const reasoningSubtitle = isReasoningInterrupted ? hasReasoningError ? 'Error' @@ -321,6 +321,7 @@ title="Reasoning" subtitle={reasoningSubtitle} preview={reasoningPreview} + overflow={reasoningOverflow} onToggle={() => toggleExpanded(index, section)} >
@@ -330,7 +331,7 @@
{:else if section.type === AgenticSectionType.REASONING_PENDING} - {@const reasoningPreview = formatReasoningPreview(section.content)} + {@const { preview: reasoningPreview, overflow: reasoningOverflow } = formatReasoningPreview(section.content)} {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'} {@const reasoningSubtitle = isStreaming ? '' : (hasReasoningError ? 'Error' : 'Cancelled')} @@ -341,6 +342,7 @@ title={reasoningTitle} subtitle={reasoningSubtitle} preview={reasoningPreview} + overflow={reasoningOverflow} {isStreaming} onToggle={() => toggleExpanded(index, section)} > diff --git a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte index f001cf4688cc..e7787b02879a 100644 --- a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte +++ b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte @@ -4,6 +4,7 @@ import { buttonVariants } from '$lib/components/ui/button/index.js'; import { Card } from '$lib/components/ui/card'; import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte'; + import { useThrottle } from '$lib/hooks/use-throttle.svelte'; import type { Snippet } from 'svelte'; import type { Component } from 'svelte'; @@ -15,6 +16,7 @@ title: string; subtitle?: string; preview?: string; + overflow?: number; isStreaming?: boolean; onToggle?: () => void; children: Snippet; @@ -28,6 +30,7 @@ title, subtitle, preview, + overflow = 0, isStreaming = false, onToggle, children @@ -35,6 +38,14 @@ let contentContainer: HTMLDivElement | undefined = $state(); + let previewKey = useThrottle(() => preview ?? '', 500); + let displayedPreview = $state(''); + + $effect(() => { + previewKey.key; + displayedPreview = preview ?? ''; + }); + const autoScroll = createAutoScrollController(); $effect(() => { @@ -60,9 +71,7 @@ class={className} > - +
{#if IconComponent} @@ -77,7 +86,14 @@
{#if preview} -
{preview}
+
+
+ {displayedPreview} +
+ {#if overflow > 0} + {overflow}+ chars + {/if} +
{/if}
diff --git a/tools/ui/src/lib/hooks/use-throttle.svelte.ts b/tools/ui/src/lib/hooks/use-throttle.svelte.ts new file mode 100644 index 000000000000..0795519787b8 --- /dev/null +++ b/tools/ui/src/lib/hooks/use-throttle.svelte.ts @@ -0,0 +1,32 @@ +/** + * Creates a reactive throttle key that increments when `getValue()` changes + * and the throttle window has elapsed since the last increment. + * + * Useful for throttling animations that should not fire on every rapid update. + * + * @param getValue - A reactive getter for the value to watch + * @param ms - Throttle window in milliseconds + * @returns A reactive number that increments when the throttled value changes + */ +export function useThrottle(getValue: () => string | undefined, ms: number) { + let key = $state(0); + let throttleEnd = $state(0); + let lastValue: string | undefined = getValue(); + + $effect(() => { + const value = getValue(); + if (value === lastValue) return; + const now = Date.now(); + if (now >= throttleEnd) { + lastValue = value; + key++; + throttleEnd = now + ms; + } + }); + + return { + get key() { + return key; + } + }; +} diff --git a/tools/ui/src/lib/utils/formatters.ts b/tools/ui/src/lib/utils/formatters.ts index 8b6f9bdefea2..97ad440a08d2 100644 --- a/tools/ui/src/lib/utils/formatters.ts +++ b/tools/ui/src/lib/utils/formatters.ts @@ -4,6 +4,7 @@ import { SECONDS_PER_HOUR, SHORT_DURATION_THRESHOLD, MEDIUM_DURATION_THRESHOLD, + MAX_PREVIEW_LENGTH, STRIP_MARKDOWN_PATTERNS } from '$lib/constants'; @@ -154,48 +155,33 @@ export function formatAttachmentText( } /** - * Strips markdown formatting (code blocks, bold, italic, links, etc.) - * and emoji characters from text, leaving plain readable content. - * Do note that the regex array isn't an exhaustive list - * - * @param text - Raw text that may contain markdown or emoji - * @returns Plain text with all formatting and emoji removed - */ -function stripMarkdownAndEmoji(text: string): string { - let result = text; - - for (const [pattern, replacement] of STRIP_MARKDOWN_PATTERNS) { - result = result.replace(pattern, replacement as string); - } - - return result.trim(); -} - -/** - * Derives a compact preview string from reasoning content for use in collapsed UI. - * - * Edge cases handled: - * - Markdown and emoji are stripped before processing - * - If the first non-empty line is shorter than 30 characters, - * the second non-empty line is appended with a " - " separator - * - Returns empty string if no content remains after stripping + * Strips markdown per line and returns the last non-empty cleaned line + * as a compact preview, along with a count of characters that overflow + * beyond MAX_PREVIEW_LENGTH. * * @param content - Raw reasoning content (may contain markdown/emoji) - * @returns A short, clean preview string + * @returns Object with `preview` string (truncated) and `overflow` count */ -export function formatReasoningPreview(content: string): string { - if (!content) return ''; - - const clean = stripMarkdownAndEmoji(content); - const lines = clean.split('\n').filter((l) => l.trim().length > 0); - - if (lines.length === 0) return ''; - - let preview = lines[0].trim(); - - if (preview.length < 30 && lines.length > 1) { - preview = preview + ' - ' + lines[1].trim(); +export function formatReasoningPreview(content: string): { preview: string; overflow: number } { + if (!content) return { preview: '', overflow: 0 }; + + const lines = content.split('\n'); + let lastLine = ''; + for (let i = 0; i < lines.length; i++) { + let cleaned = lines[i]; + for (const [pattern, replacement] of STRIP_MARKDOWN_PATTERNS) { + cleaned = cleaned.replace(pattern, replacement as string); + } + cleaned = cleaned.trim(); + if (cleaned.length > 0) { + lastLine = cleaned; + } } - return preview; + const fullLength = lastLine.length; + const overflow = Math.max(0, fullLength - MAX_PREVIEW_LENGTH); + if (fullLength > MAX_PREVIEW_LENGTH) { + lastLine = lastLine.slice(0, MAX_PREVIEW_LENGTH) + '...'; + } + return { preview: lastLine, overflow }; } From fbea8733d002ebe91f68bf641d70549910d13d58 Mon Sep 17 00:00:00 2001 From: MagicExists <106458387+gugugiyu@users.noreply.github.com> Date: Mon, 25 May 2026 18:49:25 +0700 Subject: [PATCH 05/14] chore: fix spacing Co-authored-by: Aleksander Grygier --- .../lib/components/app/content/CollapsibleContentBlock.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte index e7787b02879a..7125e0998f93 100644 --- a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte +++ b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte @@ -39,10 +39,11 @@ let contentContainer: HTMLDivElement | undefined = $state(); let previewKey = useThrottle(() => preview ?? '', 500); - let displayedPreview = $state(''); + let displayedPreview = $state(preview ?? ''); $effect(() => { previewKey.key; + displayedPreview = preview ?? ''; }); From 0427cd41a479f6ea95c2644381d64feb8df84c30 Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Mon, 25 May 2026 21:08:41 +0700 Subject: [PATCH 06/14] chore: refactor to requested changes --- tools/ui/src/lib/constants/formatters.ts | 30 +++++++++++--------- tools/ui/src/lib/utils/formatters.ts | 36 ++++++++++++++++-------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/tools/ui/src/lib/constants/formatters.ts b/tools/ui/src/lib/constants/formatters.ts index c84bc410dd71..5d861d5775c2 100644 --- a/tools/ui/src/lib/constants/formatters.ts +++ b/tools/ui/src/lib/constants/formatters.ts @@ -15,26 +15,30 @@ export const MAX_PREVIEW_LENGTH = 120; * Each entry is [pattern, replacement] — replacement can be a string * or a function that receives the match and returns the replacement. */ -export const STRIP_MARKDOWN_PATTERNS: [RegExp, string | ((match: string) => string)][] = [ - // Code blocks (fenced and inline) - [/^```.*/gm, ''], - [/.*```$/gm, ''], - [/`[^`]*`/g, ''], +export const STRIP_BLOCK_MARKDOWN_PATTERNS: [RegExp, string | ((match: string) => string)][] = [ + // Replaces ``` ... ``` blocks entirely + [/```[\s\S]*?```/g, ''], // HTML tags [/<[^>]*>/g, ''], - // Bold and italic markers + // Bold markers [/\*\*(.*?)\*\*/g, '$1'], [/__(.*?)__/g, '$1'], + // Emojis (strip globally) + [/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{231A}-\u{231B}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}]/gu, ''], +]; + +export const STRIP_INLINE_MARKDOWN_PATTERNS: [RegExp, string | ((match: string) => string)][] = [ + // Inline code + [/`[^`]*`/g, ''], + // Italic markers [/\*(.*?)\*/g, '$1'], [/_(.*?)_/g, '$1'], - // Blockquotes, headings, and list markers + // Blockquotes [/^>\s*/gm, ''], + // Headings [/^#{1,6}\s+/gm, ''], + // Bullet lists [/^[\s]*[-*+]\s+/gm, ''], + // Numbered lists [/^[\s]*\d+[.)]\s+/gm, ''], - // Emoji - [ - /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{231A}-\u{231B}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}]/gu, - '' - ] -]; +]; \ No newline at end of file diff --git a/tools/ui/src/lib/utils/formatters.ts b/tools/ui/src/lib/utils/formatters.ts index 97ad440a08d2..7db48460a3ea 100644 --- a/tools/ui/src/lib/utils/formatters.ts +++ b/tools/ui/src/lib/utils/formatters.ts @@ -5,7 +5,9 @@ import { SHORT_DURATION_THRESHOLD, MEDIUM_DURATION_THRESHOLD, MAX_PREVIEW_LENGTH, - STRIP_MARKDOWN_PATTERNS + STRIP_BLOCK_MARKDOWN_PATTERNS, + STRIP_INLINE_MARKDOWN_PATTERNS, + NEWLINE_SEPARATOR } from '$lib/constants'; /** @@ -164,24 +166,34 @@ export function formatAttachmentText( */ export function formatReasoningPreview(content: string): { preview: string; overflow: number } { if (!content) return { preview: '', overflow: 0 }; - - const lines = content.split('\n'); + // 1. Strip Block-Level Markdown Globally (Fastest) + // This removes fenced code blocks, HTML, and bold from the entire stream at once + let cleanedContent = content; + for (const [pattern, replacement] of STRIP_BLOCK_MARKDOWN_PATTERNS) { + cleanedContent = cleanedContent.replace(pattern, replacement as string); + } + // 2. Split and iterate BACKWARDS to find the last non-empty line + const lines = cleanedContent.split(NEWLINE_SEPARATOR); let lastLine = ''; - for (let i = 0; i < lines.length; i++) { - let cleaned = lines[i]; - for (const [pattern, replacement] of STRIP_MARKDOWN_PATTERNS) { - cleaned = cleaned.replace(pattern, replacement as string); + // Optimization: Start from the end. If the model outputs 1000 lines, + // but the answer is on line 990, we stop here instead of scanning all 1000. + for (let i = lines.length - 1; i >= 0; i--) { + let line = lines[i]; + // Apply Inline Patterns (Lists, Code, Quotes) only to this specific line + for (const [pattern, replacement] of STRIP_INLINE_MARKDOWN_PATTERNS) { + line = line.replace(pattern, replacement as string); } - cleaned = cleaned.trim(); - if (cleaned.length > 0) { - lastLine = cleaned; + const trimmed = line.trim(); + if (trimmed.length > 0) { + lastLine = trimmed; + break; // Found the most recent content, exit loop immediately } } - + if (!lastLine) return { preview: '', overflow: 0 }; const fullLength = lastLine.length; const overflow = Math.max(0, fullLength - MAX_PREVIEW_LENGTH); if (fullLength > MAX_PREVIEW_LENGTH) { lastLine = lastLine.slice(0, MAX_PREVIEW_LENGTH) + '...'; } return { preview: lastLine, overflow }; -} +} \ No newline at end of file From 976e271f724ee05890aefaf316df22b9261d7299 Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Tue, 26 May 2026 17:06:41 +0700 Subject: [PATCH 07/14] refactor: grouped by capture pattern instead of block-level + inline --- tools/ui/src/lib/constants/formatters.ts | 45 +++++++++-------------- tools/ui/src/lib/utils/formatters.ts | 46 +++++++++--------------- 2 files changed, 34 insertions(+), 57 deletions(-) diff --git a/tools/ui/src/lib/constants/formatters.ts b/tools/ui/src/lib/constants/formatters.ts index 5d861d5775c2..6a73df0869f2 100644 --- a/tools/ui/src/lib/constants/formatters.ts +++ b/tools/ui/src/lib/constants/formatters.ts @@ -10,35 +10,24 @@ export const DEFAULT_PERFORMANCE_TIME = '0s'; /** Max length before reasoning preview is truncated */ export const MAX_PREVIEW_LENGTH = 120; -/** - * Regex patterns used to strip markdown formatting from text. - * Each entry is [pattern, replacement] — replacement can be a string - * or a function that receives the match and returns the replacement. - */ -export const STRIP_BLOCK_MARKDOWN_PATTERNS: [RegExp, string | ((match: string) => string)][] = [ - // Replaces ``` ... ``` blocks entirely - [/```[\s\S]*?```/g, ''], - // HTML tags - [/<[^>]*>/g, ''], - // Bold markers +export const STRIP_MARKDOWN_CAPTURE_PATTERNS: [RegExp, string][] = [ + [/^```(.*)/gm, '$1'], + [/(.*)```$/gm, '$1'], + [/`([^`]*)`/g, '$1'], [/\*\*(.*?)\*\*/g, '$1'], [/__(.*?)__/g, '$1'], - // Emojis (strip globally) - [/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{231A}-\u{231B}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}]/gu, ''], -]; - -export const STRIP_INLINE_MARKDOWN_PATTERNS: [RegExp, string | ((match: string) => string)][] = [ - // Inline code - [/`[^`]*`/g, ''], - // Italic markers [/\*(.*?)\*/g, '$1'], [/_(.*?)_/g, '$1'], - // Blockquotes - [/^>\s*/gm, ''], - // Headings - [/^#{1,6}\s+/gm, ''], - // Bullet lists - [/^[\s]*[-*+]\s+/gm, ''], - // Numbered lists - [/^[\s]*\d+[.)]\s+/gm, ''], -]; \ No newline at end of file +]; + +export const STRIP_MARKDOWN_INLINE_REGEX = new RegExp( + [ + '<[^>]*>', + '^>\\s*', + '^#{1,6}\\s+', + '^[\\s]*[-*+]\\s+', + '^[\\s]*\\d+[.)]\\s+', + '[\\u{1F600}-\\u{1F64F}\\u{1F300}-\\u{1F5FF}\\u{1F680}-\\u{1F6FF}\\u{1F1E0}-\\u{1F1FF}\\u{2600}-\\u{26FF}\\u{2700}-\\u{27BF}\\u{FE00}-\\u{FE0F}\\u{1F900}-\\u{1F9FF}\\u{1FA00}-\\u{1FA6F}\\u{1FA70}-\\u{1FAFF}\\u{200D}\\u{20E3}\\u{231A}-\\u{231B}\\u{23E9}-\\u{23F3}\\u{23F8}-\\u{23FA}\\u{25AA}-\\u{25AB}\\u{25B6}\\u{25C0}\\u{25FB}-\\u{25FE}\\u{2934}-\\u{2935}\\u{2B05}-\\u{2B07}\\u{2B1B}-\\u{2B1C}\\u{2B50}\\u{2B55}\\u{3030}\\u{303D}\\u{3297}\\u{3299}]' + ].join('|'), + 'gmu' +); \ No newline at end of file diff --git a/tools/ui/src/lib/utils/formatters.ts b/tools/ui/src/lib/utils/formatters.ts index 7db48460a3ea..03e9ce3623cd 100644 --- a/tools/ui/src/lib/utils/formatters.ts +++ b/tools/ui/src/lib/utils/formatters.ts @@ -5,8 +5,8 @@ import { SHORT_DURATION_THRESHOLD, MEDIUM_DURATION_THRESHOLD, MAX_PREVIEW_LENGTH, - STRIP_BLOCK_MARKDOWN_PATTERNS, - STRIP_INLINE_MARKDOWN_PATTERNS, + STRIP_MARKDOWN_INLINE_REGEX, + STRIP_MARKDOWN_CAPTURE_PATTERNS, NEWLINE_SEPARATOR } from '$lib/constants'; @@ -156,44 +156,32 @@ export function formatAttachmentText( return `\n\n--- ${label}: ${header} ---\n${content}`; } -/** - * Strips markdown per line and returns the last non-empty cleaned line - * as a compact preview, along with a count of characters that overflow - * beyond MAX_PREVIEW_LENGTH. - * - * @param content - Raw reasoning content (may contain markdown/emoji) - * @returns Object with `preview` string (truncated) and `overflow` count - */ export function formatReasoningPreview(content: string): { preview: string; overflow: number } { if (!content) return { preview: '', overflow: 0 }; - // 1. Strip Block-Level Markdown Globally (Fastest) - // This removes fenced code blocks, HTML, and bold from the entire stream at once - let cleanedContent = content; - for (const [pattern, replacement] of STRIP_BLOCK_MARKDOWN_PATTERNS) { - cleanedContent = cleanedContent.replace(pattern, replacement as string); - } - // 2. Split and iterate BACKWARDS to find the last non-empty line - const lines = cleanedContent.split(NEWLINE_SEPARATOR); + + const lines = content.split(NEWLINE_SEPARATOR); let lastLine = ''; - // Optimization: Start from the end. If the model outputs 1000 lines, - // but the answer is on line 990, we stop here instead of scanning all 1000. + for (let i = lines.length - 1; i >= 0; i--) { - let line = lines[i]; - // Apply Inline Patterns (Lists, Code, Quotes) only to this specific line - for (const [pattern, replacement] of STRIP_INLINE_MARKDOWN_PATTERNS) { - line = line.replace(pattern, replacement as string); + let cleaned = lines[i].trim(); + if (!cleaned) continue; + + cleaned = cleaned.replace(STRIP_MARKDOWN_INLINE_REGEX, ''); + for (const [pattern, replacement] of STRIP_MARKDOWN_CAPTURE_PATTERNS) { + cleaned = cleaned.replace(pattern, replacement); } - const trimmed = line.trim(); - if (trimmed.length > 0) { - lastLine = trimmed; - break; // Found the most recent content, exit loop immediately + + if (cleaned.length > 0) { + lastLine = cleaned; + break; } } - if (!lastLine) return { preview: '', overflow: 0 }; + const fullLength = lastLine.length; const overflow = Math.max(0, fullLength - MAX_PREVIEW_LENGTH); if (fullLength > MAX_PREVIEW_LENGTH) { lastLine = lastLine.slice(0, MAX_PREVIEW_LENGTH) + '...'; } + return { preview: lastLine, overflow }; } \ No newline at end of file From fde29f0f5899a0d443fd575025769c019ac0a0ca Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Wed, 27 May 2026 21:13:36 +0700 Subject: [PATCH 08/14] ui: fax interrupt state only trigger for 1st reasoning message --- .../chat/ChatMessages/ChatMessageAgenticContent.svelte | 10 +--------- tools/ui/src/lib/utils/agentic.ts | 4 +++- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 3581555a26e4..2d4ba3bda6e7 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -62,14 +62,6 @@ isLastAssistantMessage ? !!agenticLastError(message.convId) : false ); - const isReasoningInterrupted = $derived( - isLastAssistantMessage && - !isStreaming && - !!message.reasoningContent && - !message.content?.trim() && - !message.toolCalls - ); - let permissionDismissed = $state(false); const pendingPermission = $derived( @@ -308,7 +300,7 @@ {:else if section.type === AgenticSectionType.REASONING} {@const { preview: reasoningPreview, overflow: reasoningOverflow } = formatReasoningPreview(section.content)} - {@const reasoningSubtitle = isReasoningInterrupted + {@const reasoningSubtitle = section.wasInterrupted ? hasReasoningError ? 'Error' : 'Cancelled' diff --git a/tools/ui/src/lib/utils/agentic.ts b/tools/ui/src/lib/utils/agentic.ts index 52ff35793063..d19f03434e6b 100644 --- a/tools/ui/src/lib/utils/agentic.ts +++ b/tools/ui/src/lib/utils/agentic.ts @@ -18,6 +18,7 @@ export interface AgenticSection { toolArgs?: string; toolResult?: string; toolResultExtras?: DatabaseMessageExtra[]; + wasInterrupted?: boolean; } /** @@ -51,7 +52,8 @@ function deriveSingleTurnSections( const isPending = isStreaming && !hasContentAfterReasoning; sections.push({ type: isPending ? AgenticSectionType.REASONING_PENDING : AgenticSectionType.REASONING, - content: message.reasoningContent + content: message.reasoningContent, + wasInterrupted: !isStreaming && !hasContentAfterReasoning }); } From f9722ac5ee30e6bfd9b0f3cec76179d2e3f3bd88 Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Tue, 2 Jun 2026 08:30:53 +0700 Subject: [PATCH 09/14] chore: make reasoning preview respects showThoughtInProgress setting --- .../chat/ChatMessages/ChatMessageAgenticContent.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 2d4ba3bda6e7..c9fd59175ae9 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -312,8 +312,8 @@ icon={Brain} title="Reasoning" subtitle={reasoningSubtitle} - preview={reasoningPreview} - overflow={reasoningOverflow} + preview={!showThoughtInProgress ? reasoningPreview : undefined} + overflow={!showThoughtInProgress ? reasoningOverflow : 0} onToggle={() => toggleExpanded(index, section)} >
@@ -333,8 +333,8 @@ icon={Brain} title={reasoningTitle} subtitle={reasoningSubtitle} - preview={reasoningPreview} - overflow={reasoningOverflow} + preview={!showThoughtInProgress ? reasoningPreview : undefined} + overflow={!showThoughtInProgress ? reasoningOverflow : 0} {isStreaming} onToggle={() => toggleExpanded(index, section)} > From c36223d344ab430c4b305906df5121251efcb768 Mon Sep 17 00:00:00 2001 From: MagicExists <106458387+gugugiyu@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:16:28 +0700 Subject: [PATCH 10/14] chore; newline at EOF Co-authored-by: Aleksander Grygier --- tools/ui/src/lib/utils/formatters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ui/src/lib/utils/formatters.ts b/tools/ui/src/lib/utils/formatters.ts index 03e9ce3623cd..091e1287b843 100644 --- a/tools/ui/src/lib/utils/formatters.ts +++ b/tools/ui/src/lib/utils/formatters.ts @@ -184,4 +184,4 @@ export function formatReasoningPreview(content: string): { preview: string; over } return { preview: lastLine, overflow }; -} \ No newline at end of file +} From 84d0d67987cb19b3842140c730688bcc04b5bd6f Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Tue, 2 Jun 2026 18:10:36 +0700 Subject: [PATCH 11/14] fix: thread rawContent so collapsible content can handle compute preview --- .../ChatMessageAgenticContent.svelte | 9 ++---- .../content/CollapsibleContentBlock.svelte | 32 +++++++++++-------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index c9fd59175ae9..9b1effe49c2e 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -22,7 +22,6 @@ } from '$lib/types'; import { deriveAgenticSections, - formatReasoningPreview, formatJsonPretty, parseToolResultWithImages, type AgenticSection, @@ -299,7 +298,6 @@
{:else if section.type === AgenticSectionType.REASONING} - {@const { preview: reasoningPreview, overflow: reasoningOverflow } = formatReasoningPreview(section.content)} {@const reasoningSubtitle = section.wasInterrupted ? hasReasoningError ? 'Error' @@ -312,8 +310,7 @@ icon={Brain} title="Reasoning" subtitle={reasoningSubtitle} - preview={!showThoughtInProgress ? reasoningPreview : undefined} - overflow={!showThoughtInProgress ? reasoningOverflow : 0} + rawContent={!showThoughtInProgress ? section.content : undefined} onToggle={() => toggleExpanded(index, section)} >
@@ -323,7 +320,6 @@
{:else if section.type === AgenticSectionType.REASONING_PENDING} - {@const { preview: reasoningPreview, overflow: reasoningOverflow } = formatReasoningPreview(section.content)} {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'} {@const reasoningSubtitle = isStreaming ? '' : (hasReasoningError ? 'Error' : 'Cancelled')} @@ -333,8 +329,7 @@ icon={Brain} title={reasoningTitle} subtitle={reasoningSubtitle} - preview={!showThoughtInProgress ? reasoningPreview : undefined} - overflow={!showThoughtInProgress ? reasoningOverflow : 0} + rawContent={!showThoughtInProgress ? section.content : undefined} {isStreaming} onToggle={() => toggleExpanded(index, section)} > diff --git a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte index 7125e0998f93..7d476f16ba15 100644 --- a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte +++ b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte @@ -5,6 +5,7 @@ import { Card } from '$lib/components/ui/card'; import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte'; import { useThrottle } from '$lib/hooks/use-throttle.svelte'; + import { formatReasoningPreview } from '$lib/utils'; import type { Snippet } from 'svelte'; import type { Component } from 'svelte'; @@ -17,6 +18,7 @@ subtitle?: string; preview?: string; overflow?: number; + rawContent?: string; isStreaming?: boolean; onToggle?: () => void; children: Snippet; @@ -31,6 +33,7 @@ subtitle, preview, overflow = 0, + rawContent, isStreaming = false, onToggle, children @@ -38,13 +41,16 @@ let contentContainer: HTMLDivElement | undefined = $state(); - let previewKey = useThrottle(() => preview ?? '', 500); - let displayedPreview = $state(preview ?? ''); + let previewKey = useThrottle(() => rawContent ?? preview ?? '', 500); + let displayedPreview = $state(''); + let displayedOverflow = $state(0); $effect(() => { previewKey.key; - - displayedPreview = preview ?? ''; + const content = rawContent ?? preview ?? ''; + const result = formatReasoningPreview(content); + displayedPreview = result.preview; + displayedOverflow = result.overflow; }); const autoScroll = createAutoScrollController(); @@ -86,16 +92,16 @@ {/if} - {#if preview} -
-
- {displayedPreview} -
- {#if overflow > 0} - {overflow}+ chars - {/if} + {#if displayedPreview} +
+
+ {displayedPreview}
- {/if} + {#if displayedOverflow > 0} + {displayedOverflow}+ chars + {/if} +
+ {/if}
Date: Thu, 4 Jun 2026 12:08:48 +0700 Subject: [PATCH 12/14] patch: showThoughtInProgress accidentally blocks rawContent being passed --- .../app/chat/ChatMessages/ChatMessageAgenticContent.svelte | 4 ++-- .../components/app/content/CollapsibleContentBlock.svelte | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 9b1effe49c2e..714b20dcf630 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -310,7 +310,7 @@ icon={Brain} title="Reasoning" subtitle={reasoningSubtitle} - rawContent={!showThoughtInProgress ? section.content : undefined} + rawContent={section.content} onToggle={() => toggleExpanded(index, section)} >
@@ -329,7 +329,7 @@ icon={Brain} title={reasoningTitle} subtitle={reasoningSubtitle} - rawContent={!showThoughtInProgress ? section.content : undefined} + rawContent={section.content} {isStreaming} onToggle={() => toggleExpanded(index, section)} > diff --git a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte index 7d476f16ba15..48ce4ea04d14 100644 --- a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte +++ b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte @@ -6,6 +6,7 @@ import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte'; import { useThrottle } from '$lib/hooks/use-throttle.svelte'; import { formatReasoningPreview } from '$lib/utils'; + import { config } from '$lib/stores/settings.svelte'; import type { Snippet } from 'svelte'; import type { Component } from 'svelte'; @@ -41,6 +42,8 @@ let contentContainer: HTMLDivElement | undefined = $state(); + const showThoughtInProgress = $derived(config().showThoughtInProgress as boolean); + let previewKey = useThrottle(() => rawContent ?? preview ?? '', 500); let displayedPreview = $state(''); let displayedOverflow = $state(0); @@ -92,7 +95,7 @@ {/if}
- {#if displayedPreview} + {#if displayedPreview && !showThoughtInProgress}
{displayedPreview} From 6b25e75d8ad3a3015476ed6422566532b6b69b0f Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Thu, 4 Jun 2026 20:35:13 +0700 Subject: [PATCH 13/14] chore: fix lint --- .../ChatMessageAgenticContent.svelte | 6 ++++-- .../content/CollapsibleContentBlock.svelte | 20 ++++++++++--------- tools/ui/src/lib/constants/formatters.ts | 4 ++-- tools/ui/src/lib/utils/formatters.ts | 10 +++++----- tools/ui/src/lib/utils/index.ts | 2 +- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 714b20dcf630..e21dff993ffd 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -302,7 +302,9 @@ ? hasReasoningError ? 'Error' : 'Cancelled' - : (isStreaming ? '' : undefined)} + : isStreaming + ? '' + : undefined} {:else if section.type === AgenticSectionType.REASONING_PENDING} {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'} - {@const reasoningSubtitle = isStreaming ? '' : (hasReasoningError ? 'Error' : 'Cancelled')} + {@const reasoningSubtitle = isStreaming ? '' : hasReasoningError ? 'Error' : 'Cancelled'} - {#if displayedPreview && !showThoughtInProgress} -
-
- {displayedPreview} + {#if displayedPreview && !showThoughtInProgress} +
+
+ {displayedPreview} +
+ {#if displayedOverflow > 0} + {displayedOverflow}+ chars + {/if}
- {#if displayedOverflow > 0} - {displayedOverflow}+ chars - {/if} -
- {/if} + {/if}
= 0; i--) { let cleaned = lines[i].trim(); if (!cleaned) continue; @@ -170,18 +170,18 @@ export function formatReasoningPreview(content: string): { preview: string; over for (const [pattern, replacement] of STRIP_MARKDOWN_CAPTURE_PATTERNS) { cleaned = cleaned.replace(pattern, replacement); } - + if (cleaned.length > 0) { lastLine = cleaned; break; } } - + const fullLength = lastLine.length; const overflow = Math.max(0, fullLength - MAX_PREVIEW_LENGTH); if (fullLength > MAX_PREVIEW_LENGTH) { lastLine = lastLine.slice(0, MAX_PREVIEW_LENGTH) + '...'; } - + return { preview: lastLine, overflow }; } diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts index fef44310152a..637db8812c4d 100644 --- a/tools/ui/src/lib/utils/index.ts +++ b/tools/ui/src/lib/utils/index.ts @@ -77,7 +77,7 @@ export { formatTime, formatPerformanceTime, formatAttachmentText, - formatReasoningPreview, + formatReasoningPreview } from './formatters'; // IME utilities From 0e4a24cf9d5c77f396b0d2a1d7e24fab41e44fdb Mon Sep 17 00:00:00 2001 From: gugugiyu Date: Thu, 4 Jun 2026 20:40:00 +0700 Subject: [PATCH 14/14] chore: change smoke test --- .../lib/components/app/content/CollapsibleContentBlock.svelte | 4 +--- tools/ui/src/lib/constants/formatters.ts | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte index 56502b3d902b..8bab55d19fb9 100644 --- a/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte +++ b/tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte @@ -18,7 +18,6 @@ title: string; subtitle?: string; preview?: string; - overflow?: number; rawContent?: string; isStreaming?: boolean; onToggle?: () => void; @@ -33,7 +32,6 @@ title, subtitle, preview, - overflow = 0, rawContent, isStreaming = false, onToggle, @@ -49,7 +47,7 @@ let displayedOverflow = $state(0); $effect(() => { - previewKey.key; + void previewKey.key; const content = rawContent ?? preview ?? ''; const result = formatReasoningPreview(content); displayedPreview = result.preview; diff --git a/tools/ui/src/lib/constants/formatters.ts b/tools/ui/src/lib/constants/formatters.ts index 81364f5e8f51..c417faea43d9 100644 --- a/tools/ui/src/lib/constants/formatters.ts +++ b/tools/ui/src/lib/constants/formatters.ts @@ -20,6 +20,7 @@ export const STRIP_MARKDOWN_CAPTURE_PATTERNS: [RegExp, string][] = [ [/_(.*?)_/g, '$1'] ]; +/* eslint-disable no-misleading-character-class */ export const STRIP_MARKDOWN_INLINE_REGEX = new RegExp( [ '<[^>]*>', @@ -31,3 +32,4 @@ export const STRIP_MARKDOWN_INLINE_REGEX = new RegExp( ].join('|'), 'gmu' ); +/* eslint-enable no-misleading-character-class */