Archived
Preserve viewport when prepending chat history
This commit is contained in:
@@ -5,6 +5,11 @@ import type { ChatLine, ChatPart } from "./shared";
|
|||||||
import { chatStyles } from "./shared";
|
import { chatStyles } from "./shared";
|
||||||
import "./FormattedText";
|
import "./FormattedText";
|
||||||
|
|
||||||
|
interface PrependScrollAnchor {
|
||||||
|
scrollTop: number;
|
||||||
|
scrollHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
function isScrollPosition(value: unknown): value is { index: number; offset: number } {
|
function isScrollPosition(value: unknown): value is { index: number; offset: number } {
|
||||||
return typeof value === "object"
|
return typeof value === "object"
|
||||||
&& value !== null
|
&& 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) {
|
saveScrollPosition(sessionId = this.sessionId) {
|
||||||
const chat = this.chat;
|
const chat = this.chat;
|
||||||
if (!chat || !sessionId) return;
|
if (!chat || !sessionId) return;
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ export class PiWebApp extends LitElement {
|
|||||||
this.promptEditor?.focusInput();
|
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() {
|
private updateUrl() {
|
||||||
writeRoute({
|
writeRoute({
|
||||||
projectId: this.state.selectedProject?.id,
|
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.error ? html`<div class="error">${state.error}</div>` : null}
|
||||||
${state.selectedSession ? html`
|
${state.selectedSession ? html`
|
||||||
<status-bar .status=${state.status} .activity=${state.activity} .workspace=${state.selectedWorkspace}></status-bar>
|
<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>
|
<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}
|
${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>`}
|
` : html`<div class="empty">Select or start a session.</div>`}
|
||||||
|
|||||||
Reference in New Issue
Block a user