Archived
Keep event group anchors stable when prepending
This commit is contained in:
@@ -13,9 +13,9 @@ describe("groupChatMessages", () => {
|
||||
];
|
||||
|
||||
expect(groupChatMessages(messages, 10)).toEqual([
|
||||
{ kind: "group", startIndex: 10, messages: [messages[0]] },
|
||||
{ kind: "group", startIndex: 10, endIndex: 10, messages: [messages[0]] },
|
||||
{ kind: "message", index: 11, message: text("assistant", "visible answer") },
|
||||
{ kind: "group", startIndex: 12, messages: [messages[2]] },
|
||||
{ kind: "group", startIndex: 12, endIndex: 12, messages: [messages[2]] },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("groupChatMessages", () => {
|
||||
];
|
||||
|
||||
expect(groupChatMessages(messages)).toEqual([
|
||||
{ kind: "group", startIndex: 0, messages: [{ role: "assistant", parts: [{ type: "thinking", text: "hidden" }] }] },
|
||||
{ kind: "group", startIndex: 0, endIndex: 0, messages: [{ role: "assistant", parts: [{ type: "thinking", text: "hidden" }] }] },
|
||||
{ kind: "message", index: 0, message: { role: "assistant", parts: [{ type: "text", text: "shown" }] } },
|
||||
]);
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe("groupChatMessages", () => {
|
||||
];
|
||||
|
||||
expect(groupChatMessages(messages)).toEqual([
|
||||
{ kind: "group", startIndex: 0, messages: [{ role: "assistant", parts: [{ type: "thinking", text: "plan" }] }] },
|
||||
{ kind: "group", startIndex: 0, endIndex: 0, messages: [{ role: "assistant", parts: [{ type: "thinking", text: "plan" }] }] },
|
||||
{ kind: "message", index: 0, message: { role: "skill", parts: [{ type: "skillRead", name: "playwright", path: "/skills/playwright/SKILL.md" }] } },
|
||||
]);
|
||||
});
|
||||
@@ -45,7 +45,7 @@ describe("groupChatMessages", () => {
|
||||
const message: ChatLine = { role: "assistant", parts: [{ type: "thinking", text: "hidden" }, { type: "text", text: "shown" }], meta: { timestamp: "2026-05-09T12:00:00.000Z", model: { provider: "test", id: "model" } } };
|
||||
|
||||
expect(groupChatMessages([message])).toEqual([
|
||||
{ kind: "group", startIndex: 0, messages: [{ role: "assistant", parts: [{ type: "thinking", text: "hidden" }], meta: message.meta }] },
|
||||
{ kind: "group", startIndex: 0, endIndex: 0, messages: [{ role: "assistant", parts: [{ type: "thinking", text: "hidden" }], meta: message.meta }] },
|
||||
{ kind: "message", index: 0, message: { role: "assistant", parts: [{ type: "text", text: "shown" }], meta: message.meta } },
|
||||
]);
|
||||
});
|
||||
@@ -59,7 +59,15 @@ describe("groupChatMessages", () => {
|
||||
const groups = groupChatMessages(messages);
|
||||
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0]).toMatchObject({ kind: "group", startIndex: 0 });
|
||||
expect(groups[0]).toMatchObject({ kind: "group", startIndex: 0, endIndex: 1 });
|
||||
});
|
||||
|
||||
it("keeps a stable group end index when older events are prepended into a group", () => {
|
||||
expect(groupChatMessages([
|
||||
{ role: "tool", parts: [{ type: "toolResult", toolName: "read", text: "older", isError: false }] },
|
||||
{ role: "assistant", parts: [{ type: "toolCall", toolName: "read", summary: "newer" }] },
|
||||
text("assistant", "answer"),
|
||||
], 8)[0]).toMatchObject({ kind: "group", startIndex: 8, endIndex: 9 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ChatLine, ChatPart } from "./components/shared";
|
||||
|
||||
export type ChatGroup =
|
||||
| { kind: "message"; message: ChatLine; index: number }
|
||||
| { kind: "group"; messages: ChatLine[]; startIndex: number };
|
||||
| { kind: "group"; messages: ChatLine[]; startIndex: number; endIndex: number };
|
||||
|
||||
export function groupChatMessages(messages: ChatLine[], indexOffset = 0): ChatGroup[] {
|
||||
const groups: ChatGroup[] = [];
|
||||
@@ -15,7 +15,7 @@ export function groupChatMessages(messages: ChatLine[], indexOffset = 0): ChatGr
|
||||
};
|
||||
const flushEvents = () => {
|
||||
if (!eventMessages.length) return;
|
||||
groups.push({ kind: "group", messages: eventMessages, startIndex: eventStartIndex });
|
||||
groups.push({ kind: "group", messages: eventMessages, startIndex: eventStartIndex, endIndex: eventStartIndex + eventMessages.length - 1 });
|
||||
eventMessages = [];
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import "./FormattedText";
|
||||
interface PrependScrollAnchor {
|
||||
scrollTop: number;
|
||||
scrollHeight: number;
|
||||
key?: string;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
function isScrollPosition(value: unknown): value is { index?: number; key?: string; offset: number } {
|
||||
@@ -88,10 +90,10 @@ export class ChatView extends LitElement {
|
||||
${this.renderHistoryBoundary()}
|
||||
${repeat(
|
||||
groupChatMessages(this.messages, this.messageStart),
|
||||
(group) => group.kind === "message" ? this.messageAnchorKey(group.index) : this.groupAnchorKey(group.startIndex),
|
||||
(group) => group.kind === "message" ? this.messageAnchorKey(group.index) : this.groupAnchorKey(group.endIndex),
|
||||
(group) => group.kind === "message"
|
||||
? this.renderMessage(group.message, group.index)
|
||||
: this.renderMessageGroup(group.messages, group.startIndex),
|
||||
: this.renderMessageGroup(group.messages, group.startIndex, group.endIndex),
|
||||
)}
|
||||
${this.renderQueuedMessages()}
|
||||
${this.renderSessionActivity()}
|
||||
@@ -211,10 +213,10 @@ export class ChatView extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMessageGroup(messages: ChatLine[], startIndex: number) {
|
||||
const key = this.groupKey(startIndex);
|
||||
private renderMessageGroup(messages: ChatLine[], startIndex: number, endIndex: number) {
|
||||
const key = this.groupKey(endIndex);
|
||||
return html`
|
||||
<details class="msg event-group" data-index=${startIndex} data-anchor-key=${this.groupAnchorKey(startIndex)} ?open=${this.openGroupKeys.has(key)} @toggle=${(event: Event) => { this.onGroupToggle(key, event); }}>
|
||||
<details class="msg event-group" data-index=${startIndex} data-anchor-key=${this.groupAnchorKey(endIndex)} ?open=${this.openGroupKeys.has(key)} @toggle=${(event: Event) => { this.onGroupToggle(key, event); }}>
|
||||
<summary>
|
||||
<b class="label">events</b>
|
||||
<span>${summarizeChatGroup(messages)}</span>
|
||||
@@ -489,14 +491,28 @@ export class ChatView extends LitElement {
|
||||
capturePrependScrollAnchor(): PrependScrollAnchor | undefined {
|
||||
const chat = this.chat;
|
||||
if (!chat) return undefined;
|
||||
return { scrollTop: chat.scrollTop, scrollHeight: chat.scrollHeight };
|
||||
const firstVisible = this.firstVisibleArticle();
|
||||
if (!firstVisible) return { scrollTop: chat.scrollTop, scrollHeight: chat.scrollHeight };
|
||||
const chatTop = chat.getBoundingClientRect().top;
|
||||
const key = firstVisible.dataset["anchorKey"];
|
||||
const anchor = { scrollTop: chat.scrollTop, scrollHeight: chat.scrollHeight };
|
||||
return key === undefined
|
||||
? anchor
|
||||
: { ...anchor, key, offset: firstVisible.getBoundingClientRect().top - chatTop };
|
||||
}
|
||||
|
||||
restorePrependScrollAnchor(anchor: PrependScrollAnchor | undefined): void {
|
||||
const chat = this.chat;
|
||||
if (!chat || !anchor) return;
|
||||
this.withSuppressedScrollSave(() => {
|
||||
const article = anchor.key === undefined ? undefined : this.articleAt({ key: anchor.key });
|
||||
if (article !== undefined && anchor.offset !== undefined) {
|
||||
const chatTop = chat.getBoundingClientRect().top;
|
||||
const currentOffset = article.getBoundingClientRect().top - chatTop;
|
||||
chat.scrollTop += currentOffset - anchor.offset;
|
||||
} else {
|
||||
chat.scrollTop = anchor.scrollTop + (chat.scrollHeight - anchor.scrollHeight);
|
||||
}
|
||||
this.lastScrollTop = chat.scrollTop;
|
||||
});
|
||||
this.requestLoadMoreIfNeeded();
|
||||
@@ -584,16 +600,16 @@ export class ChatView extends LitElement {
|
||||
return `pi-web:chat-groups:${sessionId}`;
|
||||
}
|
||||
|
||||
private groupKey(startIndex: number): string {
|
||||
return `${this.sessionId}:${String(startIndex)}`;
|
||||
private groupKey(endIndex: number): string {
|
||||
return `${this.sessionId}:${String(endIndex)}`;
|
||||
}
|
||||
|
||||
private messageAnchorKey(index: number): string {
|
||||
return `m:${String(index)}`;
|
||||
}
|
||||
|
||||
private groupAnchorKey(startIndex: number): string {
|
||||
return `g:${String(startIndex)}`;
|
||||
private groupAnchorKey(endIndex: number): string {
|
||||
return `g:${String(endIndex)}`;
|
||||
}
|
||||
|
||||
private readOpenGroupKeys(): Set<string> {
|
||||
|
||||
Reference in New Issue
Block a user