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;