From 392d726195689084444eda3d64095c58058432d4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Apr 2026 13:58:15 -0700 Subject: [PATCH] fix(chat): refocus input after submit + force scroll on new messages --- .../chat-debug/chat-debug.component.ts | 22 ++++++++------- .../lib/compositions/chat/chat.component.ts | 27 +++++++++++-------- .../chat-input/chat-input.component.ts | 7 +++++ 3 files changed, 36 insertions(+), 20 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 e47acfd48..32277e0e0 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 @@ -219,19 +219,23 @@ export class ChatDebugComponent { /** Track message count to trigger auto-scroll */ private readonly messageCount = computed(() => this.ref().messages().length); + private prevMessageCount = 0; + constructor() { - // Auto-scroll to bottom when new messages arrive or loading state changes effect(() => { - this.messageCount(); // track + const count = this.messageCount(); this.ref().isLoading(); // track const el = this.scrollContainer()?.nativeElement; - if (el) { - const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150; - if (isNearBottom) { - requestAnimationFrame(() => { - el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); - }); - } + if (!el) return; + + const isNewMessage = count !== this.prevMessageCount; + this.prevMessageCount = count; + + const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150; + if (isNewMessage || isNearBottom) { + requestAnimationFrame(() => { + el.scrollTo({ top: el.scrollHeight, behavior: isNewMessage ? 'instant' : 'smooth' }); + }); } }); } diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index 4c2ba586b..44c8e66b0 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -188,21 +188,26 @@ export class ChatComponent { /** Track message count to trigger auto-scroll */ private readonly messageCount = computed(() => this.ref().messages().length); + private prevMessageCount = 0; + constructor() { - // 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. + // Auto-scroll to bottom: + // - Always scroll when message count increases (new message sent/received) + // - During streaming partials, only scroll if user is near bottom effect(() => { - this.messageCount(); // track + const count = this.messageCount(); this.ref().isLoading(); // track const el = this.scrollContainer()?.nativeElement; - if (el) { - const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150; - if (isNearBottom) { - requestAnimationFrame(() => { - el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); - }); - } + if (!el) return; + + const isNewMessage = count !== this.prevMessageCount; + this.prevMessageCount = count; + + const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150; + if (isNewMessage || isNearBottom) { + requestAnimationFrame(() => { + el.scrollTo({ top: el.scrollHeight, behavior: isNewMessage ? 'instant' : 'smooth' }); + }); } }); } diff --git a/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts b/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts index a5b453496..49beb891b 100644 --- a/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts +++ b/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts @@ -5,6 +5,8 @@ import { input, output, signal, + viewChild, + ElementRef, ChangeDetectionStrategy, } from '@angular/core'; import { FormsModule } from '@angular/forms'; @@ -49,6 +51,7 @@ export function submitMessage( (focus)="focused.set(true)" (blur)="focused.set(false)" rows="1" + #textareaEl class="flex-1 bg-transparent border-0 outline-none resize-none max-h-[120px] overflow-y-auto" [style.color]="'var(--chat-text)'" [style.fontFamily]="'var(--chat-font-family)'" @@ -79,11 +82,15 @@ export class ChatInputComponent { readonly isDisabled = computed(() => this.ref().isLoading()); readonly focused = signal(false); + private readonly textareaEl = viewChild>('textareaEl'); + onSubmit(): void { const submitted = submitMessage(this.ref(), this.messageText()); if (submitted !== null) { this.submitted.emit(submitted); this.messageText.set(''); + // Re-focus the textarea after submit + requestAnimationFrame(() => this.textareaEl()?.nativeElement.focus()); } }