From a9f418a9ea9a28f4dfcfe5d406ad198c646ab08b Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:53:10 -0700 Subject: [PATCH] fix(stream-resource): persist thread ID across messages + smooth scroll Two critical fixes: 1. Thread persistence: The bridge's onThreadId callback was not connected to currentThreadId tracking. Each submit() created a brand new thread, losing all conversation history. Fix: wrap the transport's onThreadId to update currentThreadId before calling the consumer's callback. 2. Smooth scroll: Replace setTimeout + scrollTop assignment with requestAnimationFrame + scrollTo({behavior:'smooth'}). Only auto-scrolls when user is near bottom (<150px), so reading earlier messages isn't interrupted by new content. --- .../compositions/chat-debug/chat-debug.component.ts | 7 ++++++- libs/chat/src/lib/compositions/chat/chat.component.ts | 11 +++++++++-- .../src/lib/internals/stream-manager.bridge.ts | 10 +++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts index bef110142..e47acfd48 100644 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts @@ -226,7 +226,12 @@ export class ChatDebugComponent { this.ref().isLoading(); // track const el = this.scrollContainer()?.nativeElement; if (el) { - setTimeout(() => el.scrollTop = el.scrollHeight, 0); + const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150; + if (isNearBottom) { + requestAnimationFrame(() => { + el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); + }); + } } }); } diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index b869dd0f7..4c2ba586b 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -189,13 +189,20 @@ export class ChatComponent { private readonly messageCount = computed(() => this.ref().messages().length); constructor() { - // Auto-scroll to bottom when new messages arrive or loading state changes + // Auto-scroll to bottom when new messages arrive. + // Only scrolls if user is already near the bottom (within 150px), + // so reading earlier messages isn't interrupted. effect(() => { this.messageCount(); // track this.ref().isLoading(); // track const el = this.scrollContainer()?.nativeElement; if (el) { - setTimeout(() => el.scrollTop = el.scrollHeight, 0); + const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150; + if (isNearBottom) { + requestAnimationFrame(() => { + el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); + }); + } } }); } diff --git a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts index 0620a6a69..a59a8f52b 100644 --- a/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts +++ b/libs/stream-resource/src/lib/internals/stream-manager.bridge.ts @@ -30,8 +30,16 @@ export interface StreamManagerBridge { export function createStreamManagerBridge( { options, subjects, threadId$, destroy$ }: StreamManagerBridgeOptions ): StreamManagerBridge { + // Intercept onThreadId to update currentThreadId when the transport + // auto-creates a thread. Without this, each submit() creates a new thread + // because currentThreadId stays null. + const userOnThreadId = options.onThreadId; + const wrappedOnThreadId = (id: string) => { + currentThreadId = id; + userOnThreadId?.(id); + }; const transport: StreamResourceTransport = - options.transport ?? new FetchStreamTransport(options.apiUrl, options.onThreadId); + options.transport ?? new FetchStreamTransport(options.apiUrl, wrappedOnThreadId); let currentThreadId: string | null = null; let lastPayload: unknown = null;