Keep the transcript pinned to the bottom as its content changes

This commit is contained in:
Ian Gulliver
2026-10-15 13:49:10 -07:00
parent cd0664e1e0
commit 8da51ea9ba
5 changed files with 49 additions and 29 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+22 -13
View File
@@ -112,6 +112,20 @@ export function mountChat(root, src, makeHead) {
const head = h("header", { class: "task-header" });
const transcript = h("div", { class: "transcript" });
const composer = h("div", { class: "composer" });
let stuck = true;
const pin = () => {
if (stuck) {
transcript.scrollTop = transcript.scrollHeight;
}
};
transcript.addEventListener("scroll", () => {
stuck = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
});
transcript.addEventListener("load", pin, true);
const resizes = new ResizeObserver(pin);
resizes.observe(transcript);
const mutations = new MutationObserver(pin);
mutations.observe(transcript, { childList: true, subtree: true, characterData: true });
const visibleReports = new Map();
const acked = new Set();
const ackVisible = () => {
@@ -171,11 +185,8 @@ export function mountChat(root, src, makeHead) {
if (running === !spinner.hidden) {
return;
}
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
spinner.hidden = !running;
if (pinned) {
transcript.scrollTop = transcript.scrollHeight;
}
pin();
};
const putBefore = (el, anchor) => {
if (anchor?.parentNode === transcript) {
@@ -235,7 +246,8 @@ export function mountChat(root, src, makeHead) {
pending.push(entry);
ctx.run = undefined;
putBefore(el, spinner);
transcript.scrollTop = transcript.scrollHeight;
stuck = true;
pin();
return () => {
el.remove();
pending = pending.filter((p) => p !== entry);
@@ -299,12 +311,9 @@ export function mountChat(root, src, makeHead) {
return;
}
stream.timer = 0;
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
stream.el.hidden = quietReply.startsWith(stream.text.trim());
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
if (pinned) {
transcript.scrollTop = transcript.scrollHeight;
}
pin();
}, 100);
};
const openHandoff = () => {
@@ -328,7 +337,6 @@ export function mountChat(root, src, makeHead) {
return handoff.summary;
};
const appendEvents = (events) => {
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
let contextChanged = false;
headImpl.onEvents?.(events);
for (const ev of events) {
@@ -407,11 +415,10 @@ export function mountChat(root, src, makeHead) {
renderComposer();
}
}
if (pinned) {
transcript.scrollTop = transcript.scrollHeight;
}
pin();
};
const renderAll = () => {
stuck = true;
observer.disconnect();
visibleReports.clear();
dropStream();
@@ -450,6 +457,8 @@ export function mountChat(root, src, makeHead) {
document.removeEventListener("visibilitychange", ackVisible);
headImpl.dispose?.();
observer.disconnect();
resizes.disconnect();
mutations.disconnect();
dropStream();
};
}
+1 -1
View File
File diff suppressed because one or more lines are too long