diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz
index adc7939d3b..f68e50ef30 100644
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
diff --git a/tools/server/webui/src/lib/actions/fade-in-view.svelte.ts b/tools/server/webui/src/lib/actions/fade-in-view.svelte.ts
new file mode 100644
index 0000000000..5c726b7e45
--- /dev/null
+++ b/tools/server/webui/src/lib/actions/fade-in-view.svelte.ts
@@ -0,0 +1,54 @@
+/**
+ * Svelte action that fades in an element when it enters the viewport.
+ * Uses IntersectionObserver for efficient viewport detection.
+ *
+ * If skipIfVisible is set and the element is already visible in the viewport
+ * when the action attaches (e.g. a markdown block promoted from unstable
+ * during streaming), the fade is skipped entirely to avoid a flash.
+ */
+export function fadeInView(
+ node: HTMLElement,
+ options: { duration?: number; y?: number; skipIfVisible?: boolean } = {}
+) {
+ const { duration = 300, y = 0, skipIfVisible = false } = options;
+
+ if (skipIfVisible) {
+ const rect = node.getBoundingClientRect();
+ const isAlreadyVisible =
+ rect.top < window.innerHeight &&
+ rect.bottom > 0 &&
+ rect.left < window.innerWidth &&
+ rect.right > 0;
+
+ if (isAlreadyVisible) {
+ return;
+ }
+ }
+
+ node.style.opacity = '0';
+ node.style.transform = `translateY(${y}px)`;
+ node.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`;
+
+ $effect(() => {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ for (const entry of entries) {
+ if (entry.isIntersecting) {
+ requestAnimationFrame(() => {
+ node.style.opacity = '1';
+ node.style.transform = 'translateY(0)';
+ });
+ observer.disconnect();
+ }
+ }
+ },
+ { threshold: 0.05 }
+ );
+
+ observer.observe(node);
+
+ return () => {
+ observer.disconnect();
+ };
+ });
+}
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
index 553c3fd946..8b07b385fb 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
@@ -3,14 +3,12 @@
ChatMessageAgenticContent,
ChatMessageActions,
ChatMessageStatistics,
- MarkdownContent,
ModelBadge,
ModelsSelector
} from '$lib/components/app';
import { getMessageEditContext } from '$lib/contexts';
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
import { isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
- import { agenticStreamingToolCall } from '$lib/stores/agentic.svelte';
import { autoResizeTextarea, copyToClipboard, isIMEComposing } from '$lib/utils';
import { tick } from 'svelte';
import { fade } from 'svelte/transition';
@@ -87,13 +85,7 @@
const hasAgenticMarkers = $derived(
messageContent?.includes(AGENTIC_TAGS.TOOL_CALL_START) ?? false
);
- const hasStreamingToolCall = $derived(
- isChatStreaming() && agenticStreamingToolCall(message.convId) !== null
- );
const hasReasoningMarkers = $derived(messageContent?.includes(REASONING_TAGS.START) ?? false);
- const isStructuredContent = $derived(
- hasAgenticMarkers || hasReasoningMarkers || hasStreamingToolCall
- );
const processingState = useProcessingState();
let currentConfig = $derived(config());
@@ -256,15 +248,13 @@
{:else if message.role === MessageRole.ASSISTANT}
{#if showRawOutput}
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
index 23143c955c..439e8adb38 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
@@ -1,4 +1,5 @@
-
+
{#each displayMessages as { message, isLastAssistantMessage, siblingInfo } (message.id)}
-
+
+
+
{/each}
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
index 00ddea4649..290a277a53 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
@@ -12,7 +12,6 @@
} from '$lib/components/app';
import * as Alert from '$lib/components/ui/alert';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
- import { INITIAL_SCROLL_DELAY } from '$lib/constants';
import { KeyboardKey } from '$lib/enums';
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import {
@@ -48,7 +47,7 @@
let showFileErrorDialog = $state(false);
let uploadedFiles = $state
([]);
- const autoScroll = createAutoScrollController();
+ const autoScroll = createAutoScrollController({ isColumnReverse: true });
let fileErrorData = $state<{
generallyUnsupported: File[];
@@ -310,13 +309,15 @@
afterNavigate(() => {
if (!disableAutoScroll) {
- setTimeout(() => autoScroll.scrollToBottom('instant'), INITIAL_SCROLL_DELAY);
+ autoScroll.enable();
}
});
onMount(() => {
+ autoScroll.startObserving();
+
if (!disableAutoScroll) {
- setTimeout(() => autoScroll.scrollToBottom('instant'), INITIAL_SCROLL_DELAY);
+ autoScroll.enable();
}
const pendingDraft = chatStore.consumePendingDraft();
@@ -333,10 +334,6 @@
$effect(() => {
autoScroll.setDisabled(disableAutoScroll);
});
-
- $effect(() => {
- autoScroll.updateInterval(isCurrentConversationLoading);
- });
{#if isDragOver}
@@ -351,7 +348,7 @@
-
{
- autoScroll.enable();
- autoScroll.scrollToBottom();
- }}
- />
+
+
{
+ autoScroll.enable();
+ autoScroll.scrollToBottom();
+ }}
+ />
-
-
+
+
- {#if hasPropsError}
-
-
-
-
- Server unavailable
-
-
- {serverError()}
-
+ {#if hasPropsError}
+
+
+
+
+ Server unavailable
+
+
+ {serverError()}
+
+
+ {/if}
+
+
+ chatStore.stopGeneration()}
+ onSystemPromptAdd={handleSystemPromptAdd}
+ showHelperText={false}
+ bind:uploadedFiles
+ />
- {/if}
-
-
- chatStore.stopGeneration()}
- onSystemPromptAdd={handleSystemPromptAdd}
- showHelperText={false}
- bind:uploadedFiles
- />
diff --git a/tools/server/webui/src/lib/components/app/content/MarkdownContent.svelte b/tools/server/webui/src/lib/components/app/content/MarkdownContent.svelte
index 0b10e14008..9976ffa7cc 100644
--- a/tools/server/webui/src/lib/components/app/content/MarkdownContent.svelte
+++ b/tools/server/webui/src/lib/components/app/content/MarkdownContent.svelte
@@ -36,6 +36,7 @@
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import type { DatabaseMessageExtra } from '$lib/types/database';
import { config } from '$lib/stores/settings.svelte';
+ import { fadeInView } from '$lib/actions/fade-in-view.svelte';
interface Props {
attachments?: DatabaseMessageExtra[];
@@ -598,7 +599,7 @@
: ''}"
>
{#each renderedBlocks as block (block.id)}
-
+
{@html block.html}
@@ -651,7 +652,6 @@
/>