Preserve viewport when prepending chat history

This commit is contained in:
Federico Jaramillo Martinez
2026-05-07 15:49:01 +02:00
parent eb59f1eb00
commit 046d41f202
2 changed files with 30 additions and 1 deletions
+20
View File
@@ -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;
+10 -1
View File
@@ -81,6 +81,15 @@ export class PiWebApp extends LitElement {
this.promptEditor?.focusInput();
}
private async withChatPrependTransition(action: () => Promise<void>) {
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`<div class="error">${state.error}</div>` : null}
${state.selectedSession ? html`
<status-bar .status=${state.status} .activity=${state.activity} .workspace=${state.selectedWorkspace}></status-bar>
<chat-view .sessionId=${state.selectedSession.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .onLoadMore=${() => this.withChatScrollTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
<chat-view .sessionId=${state.selectedSession.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
<prompt-editor .sessionId=${state.selectedSession.id} .cwd=${state.selectedWorkspace?.path} .onSend=${(text: string) => this.sessions.send(text)} .onStopSession=${() => this.sessions.stopSession()}></prompt-editor>
${state.commandDialog !== undefined ? html`<command-picker .title=${state.commandDialog.title} .options=${state.commandDialog.options} .onPick=${(value: string) => this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}></command-picker>` : null}
` : html`<div class="empty">Select or start a session.</div>`}