diff --git a/src/client/src/components/ChatView.ts b/src/client/src/components/ChatView.ts index a0b92b6..44f4681 100644 --- a/src/client/src/components/ChatView.ts +++ b/src/client/src/components/ChatView.ts @@ -5,6 +5,11 @@ import type { ChatLine, ChatPart } from "./shared"; import { chatStyles } from "./shared"; import "./FormattedText"; +interface PrependScrollAnchor { + scrollTop: number; + scrollHeight: number; +} + function isScrollPosition(value: unknown): value is { index: number; offset: number } { return typeof value === "object" && value !== null @@ -201,6 +206,21 @@ export class ChatView extends LitElement { }); } + capturePrependScrollAnchor(): PrependScrollAnchor | undefined { + const chat = this.chat; + if (!chat) return undefined; + return { scrollTop: chat.scrollTop, scrollHeight: chat.scrollHeight }; + } + + restorePrependScrollAnchor(anchor: PrependScrollAnchor | undefined): void { + const chat = this.chat; + if (!chat || !anchor) return; + this.withSuppressedScrollSave(() => { + chat.scrollTop = anchor.scrollTop + (chat.scrollHeight - anchor.scrollHeight); + }); + this.updateLoadedScrollPercent(); + } + saveScrollPosition(sessionId = this.sessionId) { const chat = this.chat; if (!chat || !sessionId) return; diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts index 5baa462..7f9c43b 100644 --- a/src/client/src/components/PiWebApp.ts +++ b/src/client/src/components/PiWebApp.ts @@ -81,6 +81,15 @@ export class PiWebApp extends LitElement { this.promptEditor?.focusInput(); } + private async withChatPrependTransition(action: () => Promise) { + const anchor = this.chatView?.capturePrependScrollAnchor(); + await action(); + await this.updateComplete; + await this.chatView?.updateComplete; + await nextFrame(); + this.chatView?.restorePrependScrollAnchor(anchor); + } + private updateUrl() { writeRoute({ projectId: this.state.selectedProject?.id, @@ -106,7 +115,7 @@ export class PiWebApp extends LitElement { ${state.error ? html`
${state.error}
` : null} ${state.selectedSession ? html` - 0} .loadingMore=${state.isLoadingEarlierMessages} .onLoadMore=${() => this.withChatScrollTransition(() => this.sessions.loadEarlierMessages())}> + 0} .loadingMore=${state.isLoadingEarlierMessages} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}> this.sessions.send(text)} .onStopSession=${() => this.sessions.stopSession()}> ${state.commandDialog !== undefined ? html` this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}>` : null} ` : html`
Select or start a session.
`}