ui: added single line reasoning preview (#23601)

* webui: added single line reasoning preview.

* patch: reduce width slightly for the previewing section

* refactor: move formatter constants to the right file

* feat: reimplement reasoning preview with throttled dynamic per-line rendering

* chore: fix spacing

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>

* chore: refactor to requested changes

* refactor: grouped by capture pattern instead of block-level + inline

* ui: fax interrupt state only trigger for 1st reasoning message

* chore: make reasoning preview respects showThoughtInProgress setting

* chore; newline at EOF

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>

* fix: thread rawContent so collapsible content can handle compute preview

* patch: showThoughtInProgress accidentally blocks rawContent being passed

* chore: fix lint

* chore: change smoke test

---------

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>
This commit is contained in:
MagicExists
2026-06-04 16:09:43 +02:00
committed by GitHub
co-authored by Aleksander Grygier
parent 0dbfa66a1f
commit 526977068f
7 changed files with 161 additions and 13 deletions
@@ -31,7 +31,8 @@
agenticPendingPermissionRequest,
agenticResolvePermission,
agenticPendingContinueRequest,
agenticResolveContinue
agenticResolveContinue,
agenticLastError
} from '$lib/stores/agentic.svelte';
import { config } from '$lib/stores/settings.svelte';
@@ -56,6 +57,10 @@
const showToolCallInProgress = $derived(config().showToolCallInProgress as boolean);
const showThoughtInProgress = $derived(config().showThoughtInProgress as boolean);
const hasReasoningError = $derived(
isLastAssistantMessage ? !!agenticLastError(message.convId) : false
);
let permissionDismissed = $state(false);
const pendingPermission = $derived(
@@ -293,11 +298,21 @@
</div>
</CollapsibleContentBlock>
{:else if section.type === AgenticSectionType.REASONING}
{@const reasoningSubtitle = section.wasInterrupted
? hasReasoningError
? 'Error'
: 'Cancelled'
: isStreaming
? ''
: undefined}
<CollapsibleContentBlock
open={isExpanded(index, section)}
class="my-2"
icon={Brain}
title="Reasoning"
subtitle={reasoningSubtitle}
rawContent={section.content}
onToggle={() => toggleExpanded(index, section)}
>
<div class="pt-3">
@@ -308,7 +323,7 @@
</CollapsibleContentBlock>
{:else if section.type === AgenticSectionType.REASONING_PENDING}
{@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'}
{@const reasoningSubtitle = isStreaming ? '' : 'incomplete'}
{@const reasoningSubtitle = isStreaming ? '' : hasReasoningError ? 'Error' : 'Cancelled'}
<CollapsibleContentBlock
open={isExpanded(index, section)}
@@ -316,6 +331,7 @@
icon={Brain}
title={reasoningTitle}
subtitle={reasoningSubtitle}
rawContent={section.content}
{isStreaming}
onToggle={() => toggleExpanded(index, section)}
>
@@ -4,6 +4,9 @@
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 { formatReasoningPreview } from '$lib/utils';
import { config } from '$lib/stores/settings.svelte';
import type { Snippet } from 'svelte';
import type { Component } from 'svelte';
@@ -14,6 +17,8 @@
iconClass?: string;
title: string;
subtitle?: string;
preview?: string;
rawContent?: string;
isStreaming?: boolean;
onToggle?: () => void;
children: Snippet;
@@ -26,6 +31,8 @@
iconClass = 'h-4 w-4',
title,
subtitle,
preview,
rawContent,
isStreaming = false,
onToggle,
children
@@ -33,6 +40,20 @@
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);
$effect(() => {
void previewKey.key;
const content = rawContent ?? preview ?? '';
const result = formatReasoningPreview(content);
displayedPreview = result.preview;
displayedOverflow = result.overflow;
});
const autoScroll = createAutoScrollController();
$effect(() => {
@@ -58,16 +79,31 @@
class={className}
>
<Card class="gap-0 border-muted bg-muted/30 py-0">
<Collapsible.Trigger class="flex w-full cursor-pointer items-center justify-between p-3">
<div class="flex items-center gap-2 text-muted-foreground">
{#if IconComponent}
<IconComponent class={iconClass} />
{/if}
<Collapsible.Trigger class="flex w-full cursor-pointer items-start justify-between gap-2 p-3">
<div class="flex min-w-0 items-center gap-2">
<div class="flex items-center gap-2 text-muted-foreground">
{#if IconComponent}
<IconComponent class={iconClass} />
{/if}
<span class="font-mono text-sm font-medium">{title}</span>
<span class="font-mono text-sm font-medium">{title}</span>
{#if subtitle}
<span class="text-xs italic">{subtitle}</span>
{#if subtitle}
<span class="text-xs italic">{subtitle}</span>
{/if}
</div>
{#if displayedPreview && !showThoughtInProgress}
<div class="flex min-w-0 items-baseline justify-between gap-2">
<div class="w-3/4 truncate text-xs text-muted-foreground/80">
{displayedPreview}
</div>
{#if displayedOverflow > 0}
<span class="shrink-0 text-xs text-muted-foreground/60"
>{displayedOverflow}+ chars</span
>
{/if}
</div>
{/if}
</div>